[
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn 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.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject 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.\n\nProject 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.\n\n## Scope\n\nThis 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.\n\n## Enforcement\n\nInstances 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.\n\nProject 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.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Contributing to this project\n============================\n\nThank you for considering to add to this project! Before you start writing code, here's a few tips:\n\nSmall changes / Bugfixes\n------------------------\n\nMake sure you target the `version-2.x` branch when you're working on your change and submitting your PR.\nThe `main` branch will become Version 3, which is currently unreleased and will have a few breaking changes,\nsuch as switching to ESM and dropping support for older Node versions.\n\nThere's no release date for 3.x, so if you want your compact change in the next release: base it on `version-2.x`.\n\nLarge changes / Major features / new OAuth2 flow/features/grant types\n---------------------------------------------------------------------\n\nDrop me a line first with your plan before you start! I'm very open to adding things from the OAuth2 ecosystem,\nbut getting alignment on the approach can potentially save time. Since I'm likely the person to maintain your\nfeature after it was merged, I want to have high confidence I understand it really well, and I'm still learning\nthis massive ecosystem.\n\nOpenID Connect\n--------------\n\nCurrently this library will not expand it's scope to support OpenID Connect. The main reason is that it requires\nbringing in a JWT library, which is in conflict with the design goal of making a 0-dependency, lean OAuth2 library.\n\nThis may change in the future, but right now OpenID is not a goal.\n\nHowever, I am open to small additions / new parameters to this library from the OpenID suite of standards.\nFor example, this library supports the `response_mode` parameter and will return the `id_token` if it's in the\nresponse.\n\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main, 'version-2.x' ]\n\njobs:\n  test:\n    name: Run tests\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [18.x, 20.x, 22.x]\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n    - run: npm ci\n    - run: npm run build --if-present\n    - run: npx tsx --test test/*.ts\n\n  lint:\n    name: Lint\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: 20.x\n    - run: npm ci\n    - run: npm run lint\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages\n\nname: Publish NPM package\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 22\n      - run: npm ci\n      - run: npm test\n\n  publish-npm:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 22\n          registry-url: https://registry.npmjs.org/\n      - run: npm ci\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}\n\n  publish-gpr:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 22\n      - run: npm ci\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n          registry-url: 'https://npm.pkg.github.com'\n          scope: '@badgateway'\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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_output\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-2025 Evert Pot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "SOURCE_FILES:=$(shell find src/ -type f -name '*.ts')\n\n.PHONY:build\nbuild: dist/build browser/oauth2-client.min.js browser/oauth2-client.min.js.gz\n\n.PHONY:test\ntest:\n\t#npx tsx --test test/*.ts\n\tnode --experimental-strip-types --test test/*.ts\n\n.PHONY:lint\nlint:\n\t./node_modules/.bin/eslint --quiet 'src/**/*.ts' 'test/**/*.ts'\n\n.PHONY:fix\nfix:\n\t./node_modules/.bin/eslint --quiet 'src/**/*.ts' 'test/**/*.ts' --fix\n\n.PHONY:watch\nwatch:\n\t./node_modules/.bin/tsc --watch\n\n\n.PHONY:clean\nclean:\n\trm -r browser/\n\trm -r dist/\n\ndist/build: ${SOURCE_FILES}\n\t./node_modules/.bin/tsc\n\ttouch dist/build\n\nbrowser/oauth2-client.min.js: ${SOURCE_FILES} vite.config.js\n\tmkdir -p browser\n\tnpx vite build\n\nbrowser/oauth2-client.min.js.gz: browser/oauth2-client.min.js\n\tgzip --keep -f browser/oauth2-client.min.js\n\t@# For some reason gzip makes the output file older than the input, so\n\t@# just making sure it gets a good mtime.\n\ttouch browser/oauth2-client.min.js.gz\n"
  },
  {
    "path": "README.md",
    "content": "# OAuth2 client for Node and browsers\n\nThis package contains an OAuth2 client. It aims to be a fully-featured OAuth2\nutility library, for Node.js, Browsers and written in Typescript.\n\nThis OAuth2 client is only **4KB** gzipped, it has **0** dependencies and\nrelies on modern APIs like `fetch()` and [Web Crypto][4] which are built-in\nsince Node 18.\n\n\n## Highlights\n\n* 16KB minified (5KB gzipped).\n* No dependencies.\n* `authorization_code` grant with optional [PKCE][1] support.\n* `password` and `client_credentials` grant.\n* a `fetch()` wrapper that automatically adds Bearer tokens and refreshes them.\n* OAuth2 endpoint discovery via the Server metadata document ([RFC8414][2]).\n* OAuth2 Token Introspection ([RFC7662][3]).\n* Resource Indicators for OAuth 2.0 ([RFC8707][5]).\n* OAuth2 Token Revocation ([RFC7009][6]).\n* [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)\n\n## Installation\n\n```sh\nnpm i @badgateway/oauth2-client\n```\n\n\n## Usage\n\nTo get started, set up the Client class.\n\n\n```typescript\nimport { OAuth2Client } from '@badgateway/oauth2-client';\n\nconst client = new OAuth2Client({\n\n  // The base URI of your OAuth2 server\n  server: 'https://my-auth-server/',\n\n  // OAuth2 client id\n  clientId: '...',\n\n  // OAuth2 client secret. Only required for 'client_credentials', 'password'\n  // flows. Don't specify this in insecure contexts, such as a browser using\n  // the authorization_code flow.\n  clientSecret: '...',\n\n\n  // The following URIs are all optional. If they are not specified, we will\n  // attempt to discover them using the oauth2 discovery document.\n  // If your server doesn't have support this, you may need to specify these.\n  // you may use relative URIs for any of these.\n\n\n  // Token endpoint. Most flows need this.\n  // If not specified we'll use the information for the discovery document\n  // first, and otherwise default to /token\n  tokenEndpoint: '/token',\n\n  // Authorization endpoint.\n  //\n  // You only need this to generate URLs for authorization_code flows.\n  // If not specified we'll use the information for the discovery document\n  // first, and otherwise default to /authorize\n  authorizationEndpoint: '/authorize',\n\n  // OAuth2 Metadata discovery endpoint.\n  //\n  // This document is used to determine various server features.\n  // If not specified, we assume it's on /.well-known/oauth2-authorization-server\n  discoveryEndpoint: '/.well-known/oauth2-authorization-server',\n\n});\n```\n\n### Tokens\n\nMany functions use or return a 'OAuth2Token' type. This type has the following\nshape:\n\n```typescript\nexport type OAuth2Token = {\n  accessToken: string;\n  refreshToken: string | null;\n\n  /**\n   * When the Access Token expires.\n   *\n   * This is expressed as a unix timestamp in milliseconds.\n   */\n  expiresAt: number | null;\n\n\n  /**\n   * If the server returned an OpenID Connect ID token, it will be stored here.\n   */\n  idToken?: string;\n\n};\n```\n\n\n### client_credentials grant.\n\n```typescript\nconst token = await client.clientCredentials();\n```\n\n### Refreshing tokens\n\n```typescript\nconst newToken = await client.refreshToken(oldToken);\n```\n\n\n### password grant:\n\n```typescript\nconst token = await client.password({\n  username: '..',\n  password: '..',\n});\n```\n\n### authorization_code\n\nThe `authorization_code` flow is the flow for browser-based applications,\nand roughly consists of 3 major steps:\n\n1. Redirect the user to an authorization endpoint, where they log in.\n2. Authorization endpoint redirects back to app with a 'code' query\n   parameter.\n3. The `code` is exchanged for a access and refresh token.\n\nThis library provides support for these steps, but there's no requirement\nto use its functionality as the system is mostly stateless.\n\n```typescript\nimport { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client';\n\nconst client = new OAuth2Client({\n  server: 'https://authserver.example/',\n  clientId: '...',\n\n  // Note, if urls cannot be auto-detected, also specify these:\n  tokenEndpoint: '/token',\n  authorizationEndpoint: '/authorize',\n});\n```\n\n**Redirecting the user to the authorization server**\n\n```typescript\n/**\n * This generates a security code that must be passed to the various steps.\n * This is used for 'PKCE' which is an advanced security feature.\n *\n * It doesn't break servers that don't support it, but it makes servers that\n * so support it more secure.\n *\n * It's optional to pass this, but recommended.\n */\nconst codeVerifier = await generateCodeVerifier();\n\n// In a browser this might work as follows:\ndocument.location = await client.authorizationCode.getAuthorizeUri({\n\n  // URL in the app that the user should get redirected to after authenticating\n  redirectUri: 'https://my-app.example/',\n\n  // Optional string that can be sent along to the auth server. This value will\n  // be sent along with the redirect back to the app verbatim.\n  state: 'some-string',\n\n  codeVerifier,\n\n  scope: ['scope1', 'scope2'],\n\n});\n```\n\n**Handling the redirect back to the app and obtain token**\n\n```typescript\nconst oauth2Token = await client.authorizationCode.getTokenFromCodeRedirect(\n  document.location,\n  {\n    /**\n     * The redirect URI is not actually used for any redirects, but MUST be the\n     * same as what you passed earlier to \"authorizationCode\"\n     */\n    redirectUri: 'https://my-app.example/',\n\n    /**\n     * This is optional, but if it's passed then it also MUST be the same as\n     * what you passed in the first step.\n     *\n     * If set, it will verify that the server sent the exact same state back.\n     */\n    state: 'some-string',\n\n    codeVerifier,\n\n  }\n);\n```\n\n\n### Fetch Wrapper\n\nWhen using an OAuth2-protected API, typically you will need to obtain an Access\ntoken, and then add this token to each request using an `Authorization: Bearer`\nheader.\n\nBecause access tokens have a limited lifetime, and occasionally needs to be\nrefreshed this is a bunch of potential plumbing.\n\nTo make this easier, this library has a 'fetch wrapper'. This is effectively\njust like a regular fetch function, except it automatically adds the header\nand will automatically refresh tokens when needed.\n\nUsage:\n\n```typescript\nimport { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';\n\nconst client = new OAuth2Client({\n  server: 'https://my-auth-server',\n  clientId: 'my-client-id'\n});\n\n\nconst fetchWrapper = new OAuth2Fetch({\n  client: client,\n\n  /**\n   * You are responsible for implementing this function.\n   * it's purpose is to supply the 'initial' oauth2 token.\n   */\n  getNewToken: async () => {\n\n    // Example\n    return client.clientCredentials();\n\n    // Another example\n    return client.authorizationCode.getToken({\n      code: '..',\n      redirectUri: '..',\n    });\n\n    // You can return null to fail the process. You may want to do this\n    // when a user needs to be redirected back to the authorization_code\n    // endpoints.\n    return null;\n\n  },\n\n  /**\n   * Optional. This will be called for any fatal authentication errors.\n   */\n  onError: (err) => {\n    // err is of type Error\n  }\n\n});\n```\n\nAfter set up, you can just call `fetch` on the new object to call your API, and\nthe library will ensure there's always a `Bearer` header.\n\n```typescript\nconst response = fetchWrapper.fetch('https://my-api', {\n  method: 'POST',\n  body: 'Hello world'\n});\n```\n\n### Storing tokens for later use with FetchWrapper\n\nTo keep a user logged in between sessions, you may want to avoid full\nreauthentication. To do this, you'll need to store authentication token\nsomewhere.\n\nThe fetch wrapper has 2 functions to help with this:\n\n```typescript\n\nconst fetchWrapper = new OAuth2Fetch({\n  client: client,\n\n  getNewToken: async () => {\n\n    // See above!\n\n  },\n\n  /**\n   * This function is called whenever the active token changes. Using this is\n   * optional, but it may be used to (for example) put the token in off-line\n   * storage for later usage.\n   */\n  storeToken: (token) => {\n    document.localStorage.setItem('token-store', JSON.stringify(token));\n  },\n\n  /**\n   * Also an optional feature. Implement this if you want the wrapper to try a\n   * stored token before attempting a full re-authentication.\n   *\n   * This function may be async. Return null if there was no token.\n   */\n  getStoredToken: () => {\n    const token = document.localStorage.getItem('token-store');\n    if (token) return JSON.parse(token);\n    return null;\n  }\n\n});\n```\n\n\n### Fetch Middleware function\n\nIt might be preferable to use this library as a more traditional 'middleware'.\n\nThe OAuth2Fetch object also exposes a `mw` function that returns a middleware\nfor fetch.\n\n```typescript\nconst mw = oauth2.mw();\nconst response = mw(\n  myRequest,\n  req => fetch(req)\n);\n```\n\nThis syntax looks a bit wild if you're not used to building middlewares, but\nthis effectively allows you to 'decorate' existing request libraries with\nfunctionality from this oauth2 library.\n\nA real example using the [Ketting](https://github.com/badgateway/ketting)\nlibrary:\n\n```typescript\nimport { Client } from 'ketting';\nimport { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';\n\n/**\n * Create the oauth2 client\n */\nconst oauth2Client = new OAuth2Client({\n  server: 'https://my-auth.example',\n  clientId: 'foo',\n});\n\n/**\n * Create the 'fetch helper'\n */\nconst oauth2Fetch = new OAuth2Fetch({\n  client: oauth2Client,\n});\n\n/**\n * Add the middleware to Ketting\n */\nconst ketting = new Client('http://api-root');\nketting.use(oauth2Fetch.mw());\n```\n\n### Introspection\nIntrospection ([RFC7662][3]) lets you find more information about a token,\nsuch as whether it's valid, which user it belongs to, which oauth2 client\nwas used to generate it, etc.\n\nTo be able to use it, your authorization server must have support for the\nintrospection endpoint. It's location will be automatically detected using\nthe Metadata discovery document.\n\n```typescript\nimport { OAuth2Client } from '@badgateway/oauth2-client';\n\nconst client = new Client({\n  server: 'https://auth-server.example/',\n\n  clientId: '...',\n\n  /**\n   * Some servers require OAuth2 clientId/clientSecret to be passed.\n   * If they require it, specify it. If not it's fine to omit.\n   */\n  clientSecret: '...',\n\n});\n\n// Get a token\nconst token = client.clientCredentials();\n\n// Introspect!\nconsole.log(client.introspect(token));\n```\n\n## OAuth2 `client_id` and `client_secret` encoding\n\nOAuth2 allows users to encode the `client_id` and `client_secret` either in a\n`Authorization: Basic` header or in the `POST` request body.\n\nReal-world OAuth2 servers may support one or the other, or both. The OAuth2\nspec _requires_ that servers support the Authorization header, and don't\nrecommend using the body.\n\nBy default, this library will use the `Authorization` header. OAuth2 also\nrequires that clients percent-encode the `client_id` and `client_secret`, but\nin practice many popular servers break if you do this. By default this library\nwill *not* percent encode any characters except the `:` character.\n\nYou can change this behavior using the `authenticatioMethod` flag:\n\n```typescript\nconst client = new OAuth2Client({\n  server: 'https://auth-server.example/',\n  clientId: '...',\n  clientSecret: '...',\n  authenticationMethod: 'client_secret_post', // encode in POST body \n});\n```\n\nThe following 3 values are currently supported:\n\n* `client_secret_post` - Encode in POST body\n* `client_secret_basic` - Encode in Authorization header using the strict\n  standard rules.\n* `client_secret_basic_interop` - Encode in Authorization header using less\n  strict rules. This is the default and more likely to work with popular\n  servers (at least some Google and Ebay APIs want this).\n\nThe current OAuth 2.1 draft switches the recommendation to use\n`client_secret_post` by default instead. When that document stabilizes and gets\nreleased, this library will also switch to use `client_secret_post` by default\nin a major release.\n\nIf your OAuth2 server supports POST, we recommend you use `client_secret_post`\nas this is more likely to work without a hitch.\n\nIf you configured the client using the OAuth2 discovery document, and the\nserver indicates it prefers `client_secret_basic` we will also default to the\nstrict form.\n\n\n## Support for older Node versions\n\nThis package works out of the box with modern browsers and Node 18.\n\nFor Node 16 and below, use a 2.x version of this package and add\npolyfills. The README.md for the 2.x branch of this package contains more information\non the exact steps for older Node versions.\n\n[1]: https://datatracker.ietf.org/doc/html/rfc7636 \"Proof Key for Code Exchange by OAuth Public Clients\"\n[2]: https://datatracker.ietf.org/doc/html/rfc8414 \"OAuth 2.0 Authorization Server Metadata\"\n[3]: https://datatracker.ietf.org/doc/html/rfc7662 \"OAuth 2.0 Token Introspection\"\n[4]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API \"Web Crypto API\"\n[5]: https://datatracker.ietf.org/doc/html/rfc8707 \"https://datatracker.ietf.org/doc/html/rfc8707\"\n[6]: https://datatracker.ietf.org/doc/html/rfc7009 \"OAuth 2.0 Token Revocation\"\n"
  },
  {
    "path": "changelog.md",
    "content": "Changelog\n=========\n\n3.3.1 (2025-09-09)\n------------------\n\n* #193: Fix race condition when multiple function calls are trying to do\n  endpoint discovery. (@lukybrody)\n\n\n3.3.0 (2025-07-30)\n------------------\n\n* OAuth2 token operations may now return an `extraParams` property, which is an\n  object with all unrecognized properties. Some servers return custom\n  properties from OAuth2 responses, and this lets users get access to them.\n  (@ericleib, @blarralde)\n* The OAuth2Token object now has a `scope` property, containing a list of\n  scopes the server returned *if* the server returned this. (@ericleib)\n\n\n3.2.0 (2025-04-23)\n------------------\n\n* #180: The browser build was failing to work for vite and next.js users since\n  v3. This was probably due to the switch to ESM. Instead of trying to fix this\n  problem in Webpack, this library has switched to vite for the minified\n  browser build instead.\n\n\n3.1.1 (2025-04-17)\n------------------\n\n* Ignore auhtentication methods from Discovery document we don't support.\n\n\n3.1.0 (2025-04-14)\n------------------\n\n* #181: Revert back to percent-encoding of the `Authorization: Basic header`.\n  Even though this was more correct from a standards perspective, this is\n  causing interopability problems with popular real-world OAuth2 servers. If\n  you need strict encoding, you can opt-in using the `authorizationMethod`\n  option. We recommend using `client_secret_post` if it's possible with your\n  server. See README.md for more information on this behaviour and what the\n  possible options are.\n\n\n3.0.0 (2025-03-06)\n------------------\n\n* Dropped support for Node 14 and 16.\n* Full conversion to ESM.\n* Support for the OpenID Connect id_token. If a server returns it, we expose it\n  as `idToken`. This is a JWT and would require parsing by a JWT library to get\n  access to its information. (@drev74, @redguardtoo).\n* #171: `client_id` and `client_secret` are now percent-encoded with the most\n  strict rules as specified by RFC 6749. We weren't doing any\n  percent/urlencoding before. This is a a BC break if your secret used special\n  characters, and the server you're talking is not compliant with the OAuth2\n  spec itself (@p2004a, @panva).\n* Migrated the test suite from Mocha and Chai to node:test and node:assert\n  (@Zen-cronic).\n* Package now uses 'erasableSyntaxOnly' flag with Typescript, so it can be used\n  with node --experimental-strip-types.\n\n\n2.4.2 (2024-09-14)\n------------------\n\n* #161: Re-use old refresh_token if no new one was issued after a refresh.\n\n\n2.4.1 (2024-08-22)\n------------------\n\n* #151: Add 'Accept' header on token requests to fix a Github compatibility\n  issue.\n* #151: Throw error when we get an invalid reply from a token endpoint.\n\n\n2.4.0 (2024-07-27)\n------------------\n\n* More robust error handling. When an error is emitted, you now give you access\n  to the emitted HTTP Response and response body.\n* Support for `response_mode=fragment` in the `authorization_code` flow.\n\n\n2.3.0 (2024-02-03)\n------------------\n\n* Fix for #128: If there's no secret, we should never use Basic auth to encode\n  the `client_id`.\n* Support for the `resource` parameter from RFC 8707.\n* Add support for `scope` parameter to `refresh()`.\n* Support for RFC 7009, Token Revocation (@adambom).\n\n\n2.2.4 (2023-09-05)\n------------------\n\n* Added `extraParams` option to `getAuthorizeUri`, allowing users to add\n  non-standard arguments to the authorization URI for servers that require\n  this. (@pks1989)\n\n\n2.2.3 (2023-08-03)\n------------------\n\n* Moved the `tokenResponseToOAuth2Token` function inside the OAuth2Client\n  class, allowing users to override the parsing logic more easily.\n\n\n2.2.2 (2023-07-28)\n------------------\n\n* #111 Some documentation fixes.\n* #110: Fix race condition with `getStoredToken` and calling `fetch()`\n  immediately after constructing `FetchWrapper`.\n\n\n2.2.1 (2023-07-07)\n------------------\n\n* #15: Fix for 'TypeError: Failed to execute 'fetch' on 'Window': Illegal\n  invocation at t.OAuth2Client.request'.\n\n\n2.2.0 (2023-04-26)\n------------------\n\n* Add an option to override which \"fetch\" implementation is used. (@bradjones1)\n\n\n2.1.1 (2023-04-23)\n------------------\n\n* Re-release. Previous build had an error.\n\n\n2.1.0 (2023-04-20)\n------------------\n\n* Allow users to provide non-standard properties to `client_credentials` token\n  requests via an `extraParams` property. This is necessary to support vendors\n  like Auth0 and Kinde which both require an `audience` parameter. (@South-Paw)\n* Sending `client_id` and `client_secret` in POST request body is now\n  optionally supported. By default the credentials will still be sent in the\n  `Authorization` header, but users can opt-in to using the body. The\n  authentication method will also be discovered if an OAuth2 or OpenID\n  discovery document is used. (@parkerduckworth)\n* The fetchWrapper now has an option to disable auto-refreshing tokens.\n  (@bradjones1)\n* Bug fix: If a 'state' parameter was not used in `authorization_code`, it\n  should not be required in the redirect.\n* Tested with Node 20.\n\n\n2.0.18 (2023-04-13)\n-------------------\n\n* Didn't run `make build` before the last release, which causes some files in\n  the `dist/` directory to be out of date.\n\n\n2.0.17 (2022-10-02)\n-------------------\n\n* Correctly pass 'scope' to `authorization_code` redirects.\n\n\n2.0.16 (2022-07-22)\n-------------------\n\n* It was not possible to generate the URL to the authorization endpoint with\n  PKCE using Node, due to depending on a global `crypto` object. This is fixed\n  with fallbacks all the way back to Node 14.\n\n\n2.0.15 (2022-07-07)\n-------------------\n\n* #70: Sending the client secret is now supported with the `authorization_code`\n  flow.\n\n\n2.0.14 (2022-06-23)\n-------------------\n\n* Re-release, to publish on Github packages.\n\n\n2.0.13 (2022-06-19)\n-------------------\n\n* Fixed some docs.\n\n\n2.0.12 (2022-06-19)\n-------------------\n\n* First stable v2 release!\n* Renamed this package from `fetch-mw-oauth2` to `@badgateway/oauth2-client`.\n* #59: Scope support for `authorization_code` flow.\n\n\n2.0.11 (2022-06-17)\n-------------------\n\n* Released with alpha tag.\n* Re-published\n\n\n2.0.10 (2022-05-10)\n-------------------\n\n* Released with alpha tag.\n* Tested on Node 14, 16.\n* Added polyfills for these node versions (see README).\n* `generateCodeVerifier` is now async to support Node 14.\n\n\n2.0.9 (2022-04-26)\n------------------\n\n* Released with alpha tag.\n* Set `Content-Type` to `application/x-www-form-urlencoded`.\n\n\n2.0.8 (2022-04-26)\n------------------\n\n* Released with alpha tag.\n* Changing the `authorization_code` signature again. It's a bit hard to come up\n  with a create signature for this, especially because there's multiple steps\n  in the process, and some information has to survive these steps.\n\n\n2.0.7 (2022-04-26)\n------------------\n\n* Released with alpha tag.\n* Re-release (broken build).\n\n\n2.0.6 (2022-04-26)\n------------------\n\n* Released with alpha tag.\n* Removed redundant parameters.\n* `authorization_code` should now also work correctly without PKCE.\n* Removed some redundant arguments.\n\n\n2.0.5 (2022-04-25)\n------------------\n\n* Released with alpha tag.\n* PKCE support.\n\n\n2.0.4 (2022-04-20)\n------------------\n\n* Released with alpha tag.\n* remove `fetchMw` and add `mw()`. `mw()` now _returns_ a middleware function.\n\n\n2.0.3 (2022-04-19)\n------------------\n\n* Released with alpha tag.\n* Export `OAuth2AuthorizationCodeClient`\n* Client.authorizationCode() should not have been `async`.\n\n\n2.0.2 (2022-04-19)\n------------------\n\n* Released with alpha tag.\n* Fix format for `introspect()` function.\n\n\n2.0.1 (2022-04-19)\n------------------\n\n* Released with alpha tag.\n* Fix introspection HTTP method name.\n\n\n2.0.0 (2022-04-19)\n------------------\n\nThe 2.0 version of this library is a complete rewrite. The original scope of\nthis library was to provide a wrapper around `fetch()` to add a `Bearer` token\nand refresh this token under the hood, but it has now evolved into a\nfull-featured modern OAuth2 library. The existing 'fetch wrapper' still exists,\nbut it's not merely one of the features this package offers. The API has\nchanges, and while I think it shouldn't be difficult to migrate, v2 offers no\nbackwards compatibility so some rewrites will be required. New features\ninclude:\n\n* Complete support for the `authorization_code` flow, including generating\n  redirect urls and parsing query parameters after redirect.\n* Support for OAuth2 endpoint discovery, using the OAuth2 Authorization Server\n  Metadata document. If your server supports it, just give the library a URL\n  and it will figure out the rest. [RFC8414][2].\n* Support for OAuth2 token introspection ([RFC7662][3]).\n* Generally a better abstraction of the OAuth2 framework.\n* Released with alpha tag.\n\n\n1.0.0 (2021-10-28)\n------------------\n\n* Dropped support for Node 10.\n* Fixed #45: Call `onAuthError` when a refresh fails.\n* Replaced `awesome-typescript-loader` with `ts-loader` for Webpack builds. the\n  former appears unmaintained.\n* Switched from Travis CI to Github Actions.\n\n\n0.7.7 (2021-02-22)\n------------------\n\n* Last version did not correctly build it's files.\n\n\n0.7.6 (2021-02-22)\n------------------\n\n* Better error handling when the response we got was not a standard OAuth2\n  error response body + adding information for when the Basic credentials were\n  wrong.\n* This fixes the bug when fetch-mw-oauth2 says there's an 'undefined' error.\n\n\n0.7.5 (2020-12-03)\n------------------\n\n* Fixing a few broken links in package.json. Does not alter any behavior.\n\n\n0.7.3 (2020-12-01)\n------------------\n\n* Re-publishing package. Previous version had an old build artifact.\n\n\n0.7.2 (2020-12-01)\n------------------\n\n* Fixed bug that completely broke the token flow.\n\n\n0.7.1 (2020-11-30)\n------------------\n\n* Fix bug in auto-refresh\n\n\n0.7.0 (2020-11-30)\n------------------\n\n* Ensure that only 1 refresh operation will happen in parallel. If there are\n  multiple things triggering the refresh, all will wait for the first one to\n  finish.\n* Automatically schedule a refresh operation 1 minute before the access token\n  expires, if the expiry time is known.\n* BC Break: If a token is known when setting up OAuth2, this now needs to be\n  passed as the second argument. The old behavior still works but will emit a\n  warning, and will be removed in a future release.\n* 'OAuth2Token' type is now exported.\n\n\n0.6.1 (2020-11-19)\n------------------\n\n* #34: Refresh operation failed for the `authorization_code` flow.\n\n\n0.6.0 (2020-11-09)\n------------------\n\n* Added a onAuthError event, allowing users to intercept this event and\n  re-authenticate.\n* Simplify types a bit. More duplication in the library, but this should result\n  in easier to read errors.\n* Typescript 4\n* Switch from tslint to eslint.\n* Webpack 5\n\n\n0.5.0 (2020-04-19)\n------------------\n\n* Added a `fetchMw()` function that takes a `next` argument so this package can\n  behave as a more regular middleware.\n\n\n0.4.2 (2019-12-09)\n------------------\n\n* Files were not correctly built in the last release.\n\n\n0.4.1 (2019-12-09)\n------------------\n\n* Error code 401 will be submitted when authentication fails. Before, we would\n  just forward the error code from the OAuth2 server, but this doesn't make a\n  lot of sense for a `fetch()` user, as the error might be misinterpreted as an\n  error unrelated to auth.\n\n\n0.4.0 (2019-11-06)\n------------------\n\n* Added a `getOptions()` method, which allows a user to get all current tokens\n  and store them in LocalStorage. These options can be used as-is in the\n  constructor.\n\n\n0.3.5 (2019-09-05)\n------------------\n\n* Include typescript sourcefiles in NPM package, for IDE's.\n\n\n0.3.4 (2019-03-19)\n------------------\n\n* This package now throws OAuth2Error classes for server-side errors.\n\n\n0.3.3 (2019-03-18)\n------------------\n\n* When refreshing a token, browsers don't allow re-use of the same `Request`\n  object. Now we're cloning it before use.\n\n\n0.3.2 (2019-03-13)\n------------------\n\n* When refreshing a token, and there's no `client_secret`, the `client_id` must\n  be sent in the body.\n\n\n0.3.1 (2019-03-13)\n------------------\n\n* Now correctly exporting all the right symbols.\n\n\n0.3.0 (2019-03-13)\n------------------\n\n* Library is refactored and now uses a class.\n* Support for `authorization_code` grant type.\n* Exposing some more information to uses.\n* Add a new `onTokenUpdate` hook for custom storage.\n* It's now possible to construct a client with an existing (old) Access and/or\n  refresh token.\n\n\n0.2.1 (2019-03-13)\n------------------\n\n* Shipping `dist/` instead of `src/`.\n* Making a browser build lean by not relying on `querystring` or `Buffer`.\n\n\n0.2.0 (2019-03-12)\n------------------\n\n* First public version\n* Support for `client_credentials`, `password` and `refresh_token`.\n* Will automatically attempt to refresh tokens if it knows an access token is\n  expired.\n\n[1]: https://datatracker.ietf.org/doc/html/rfc7636 \"Proof Key for Code Exchange\n     by OAuth Public Clients\"\n[2]: https://datatracker.ietf.org/doc/html/rfc8414 \"OAuth 2.0 Authorization\n     Server Metadata\"\n[3]: https://datatracker.ietf.org/doc/html/rfc7662 \"OAuth 2.0 Token\n     Introspection\"\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import typescriptEslint from \"@typescript-eslint/eslint-plugin\";\nimport globals from \"globals\";\nimport tsParser from \"@typescript-eslint/parser\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport js from \"@eslint/js\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst compat = new FlatCompat({\n    baseDirectory: __dirname,\n    recommendedConfig: js.configs.recommended,\n    allConfig: js.configs.all\n});\n\nexport default [...compat.extends(\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n), {\n    plugins: {\n        \"@typescript-eslint\": typescriptEslint,\n    },\n\n    languageOptions: {\n        globals: {\n            ...globals.browser,\n            ...globals.node,\n            Atomics: \"readonly\",\n            SharedArrayBuffer: \"readonly\",\n        },\n\n        parser: tsParser,\n        ecmaVersion: 2018,\n        sourceType: \"module\",\n    },\n\n    rules: {\n        indent: [\"error\", 2, {\n            SwitchCase: 1,\n        }],\n\n        \"linebreak-style\": [\"error\", \"unix\"],\n\n        \"no-constant-condition\": [\"error\", {\n            checkLoops: false,\n        }],\n\n        quotes: [\"error\", \"single\", {\n            allowTemplateLiterals: false,\n            avoidEscape: true,\n        }],\n\n        semi: [\"error\", \"always\"],\n\n        \"no-console\": [\"error\", {\n            allow: [\"warn\", \"error\", \"info\", \"debug\"],\n        }],\n\n        \"no-trailing-spaces\": \"error\",\n        \"eol-last\": \"error\",\n\n        \"@typescript-eslint/ban-ts-comment\": [\"error\", {\n            \"ts-expect-error\": \"allow-with-description\",\n        }],\n\n        \"@typescript-eslint/ban-tslint-comment\": \"error\",\n\n        \"@typescript-eslint/consistent-type-assertions\": [\"error\", {\n            assertionStyle: \"as\",\n            objectLiteralTypeAssertions: \"never\",\n        }],\n\n        \"@typescript-eslint/no-inferrable-types\": \"off\",\n        \"@typescript-eslint/no-explicit-any\": 0,\n        \"@typescript-eslint/no-for-in-array\": \"error\",\n        \"@typescript-eslint/no-invalid-void-type\": \"error\",\n        \"@typescript-eslint/no-namespace\": \"error\",\n        \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n\n        \"@typescript-eslint/no-unused-vars\": [\"error\", {\n            ignoreRestSiblings: true,\n            args: \"none\",\n            varsIgnorePattern: \"^_\",\n            caughtErrorsIgnorePattern: \"^_\"\n        }],\n\n        \"@typescript-eslint/prefer-for-of\": [\"error\"],\n        \"@typescript-eslint/prefer-ts-expect-error\": [\"error\"],\n    },\n}];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@badgateway/oauth2-client\",\n  \"version\": \"3.3.1\",\n  \"description\": \"OAuth2 client for browsers and Node.js. Tiny footprint, PKCE support\",\n  \"main\": \"dist/index.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"make test\",\n    \"prepublishOnly\": \"make build\",\n    \"lint\": \"make lint\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+ssh://git@github.com/badgateway/oauth2-client.git\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"oauth2\",\n    \"pkce\",\n    \"security\",\n    \"bearer\",\n    \"RFC6749\",\n    \"RFC7636\",\n    \"RFC7662\",\n    \"RFC8414\",\n    \"RFC8707\"\n  ],\n  \"author\": \"Evert Pot (https://evertpot.com)\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/badgateway/oauth2-client/issues\"\n  },\n  \"homepage\": \"https://github.com/badgateway/oauth2-client#readme\",\n  \"engines\": {\n    \"node\": \">= 18\"\n  },\n  \"devDependencies\": {\n    \"@curveball/bodyparser\": \"^1.0.0\",\n    \"@curveball/core\": \"^1.0.0\",\n    \"@curveball/http-errors\": \"^1.0.1\",\n    \"@types/node\": \"^18.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.5.0\",\n    \"@typescript-eslint/parser\": \"^8.5.0\",\n    \"eslint\": \"^9.10.0\",\n    \"ts-loader\": \"^9.2.6\",\n    \"typescript\": \"^5.0.4\",\n    \"vite\": \"^6.3.2\"\n  },\n  \"browser\": \"browser/oauth2-client.min.js\",\n  \"files\": [\n    \"dist/\",\n    \"src/\",\n    \"browser/\",\n    \"LICENSE\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "src/client/authorization-code.ts",
    "content": "import { OAuth2Client } from '../client.ts';\nimport type { OAuth2Token } from '../token.ts';\nimport type { AuthorizationCodeRequest } from '../messages.ts';\nimport { OAuth2Error } from '../error.ts';\n\ntype GetAuthorizeUrlParams = {\n  /**\n   * Where to redirect the user back to after authentication.\n   */\n  redirectUri: string;\n\n  /**\n   * The 'state' is a string that can be sent to the authentication server,\n   * and back to the redirectUri.\n   */\n  state?: string;\n\n  /**\n   * Code verifier for PKCE support. If you used this in the redirect\n   * to the authorization endpoint, you also need to use this again\n   * when getting the access_token on the token endpoint.\n   */\n  codeVerifier?: string;\n\n  /**\n   * List of scopes.\n   */\n  scope?: string[];\n\n  /**\n   * The resource the client intends to access.\n   *\n   * This is defined in RFC 8707.\n   */\n  resource?: string[] | string;\n\n  /**\n   * Any parameters listed here will be added to the query string for the authorization server endpoint.\n   */\n  extraParams?: Record<string, string>;\n\n  /**\n   * By default response parameters for the authorization_flow will be added\n   * to the query string.\n   *\n   * Some servers let you put this in the fragment instead. This may be\n   * benefical if your client is a browser, as embedding the authorization\n   * code in the fragment part of the URI prevents it from being sent back\n   * to the server.\n   *\n   * See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html\n   */\n  responseMode?: 'query' | 'fragment';\n}\n\ntype ValidateResponseResult = {\n\n  /**\n   * The authorization code. This code should be used to obtain an access token.\n   */\n  code: string;\n\n  /**\n   * List of scopes that the client requested.\n   */\n  scope?: string[];\n\n}\n\ntype GetTokenParams = {\n\n  code: string;\n\n  redirectUri: string;\n  state?: string;\n  codeVerifier?:string;\n\n  /**\n   * The resource the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string[] | string;\n\n}\n\nexport class OAuth2AuthorizationCodeClient {\n\n  client: OAuth2Client;\n\n  constructor(client: OAuth2Client) {\n\n    this.client = client;\n\n  }\n\n  /**\n   * Returns the URi that the user should open in a browser to initiate the\n   * authorization_code flow.\n   */\n  async getAuthorizeUri(params: GetAuthorizeUrlParams): Promise<string> {\n\n    const [\n      codeChallenge,\n      authorizationEndpoint\n    ] = await Promise.all([\n      params.codeVerifier ? getCodeChallenge(params.codeVerifier) : undefined,\n      this.client.getEndpoint('authorizationEndpoint')\n    ]);\n\n    const query = new URLSearchParams({\n      client_id: this.client.settings.clientId,\n      response_type: 'code',\n      redirect_uri: params.redirectUri,\n    });\n    if (codeChallenge) {\n      query.set('code_challenge_method', codeChallenge[0]);\n      query.set('code_challenge', codeChallenge[1]);\n    }\n    if (params.state) {\n      query.set('state', params.state);\n    }\n    if (params.scope) {\n      query.set('scope', params.scope.join(' '));\n    }\n\n    if (params.resource) for(const resource of [].concat(params.resource as any)) {\n      query.append('resource', resource);\n    }\n\n    if (params.responseMode && params.responseMode!=='query') {\n      query.append('response_mode', params.responseMode);\n    }\n\n    if (params.extraParams) for(const [k,v] of Object.entries(params.extraParams)) {\n      if (query.has(k)) throw new Error(`Property in extraParams would overwrite standard property: ${k}`);\n      query.set(k, v);\n    }\n\n    return authorizationEndpoint + '?' + query.toString();\n\n  }\n\n  async getTokenFromCodeRedirect(url: string|URL, params: Omit<GetTokenParams, 'code'> ): Promise<OAuth2Token> {\n\n    const { code } = this.validateResponse(url, {\n      state: params.state\n    });\n\n    return this.getToken({\n      code,\n      redirectUri: params.redirectUri,\n      codeVerifier: params.codeVerifier,\n    });\n\n  }\n\n  /**\n   * After the user redirected back from the authorization endpoint, the\n   * url will contain a 'code' and other information.\n   *\n   * This function takes the url and validate the response. If the user\n   * redirected back with an error, an error will be thrown.\n   */\n  validateResponse(url: string|URL, params: {state?: string}): ValidateResponseResult {\n\n    url = new URL(url);\n    let queryParams = url.searchParams;\n    if (!queryParams.has('code') && !queryParams.has('error') && url.hash.length>0) {\n      // Try the fragment\n      queryParams = new URLSearchParams(url.hash.slice(1));\n    }\n\n    if (queryParams.has('error')) {\n      throw new OAuth2Error(\n        queryParams.get('error_description') ?? 'OAuth2 error',\n        queryParams.get('error') as any,\n      );\n    }\n\n    if (!queryParams.has('code')) throw new Error(`The url did not contain a code parameter ${url}`);\n\n    if (params.state && params.state !== queryParams.get('state')) {\n      throw new Error(`The \"state\" parameter in the url did not match the expected value of ${params.state}`);\n    }\n\n    return {\n      code: queryParams.get('code')!,\n      scope: queryParams.has('scope') ? queryParams.get('scope')!.split(' ') : undefined,\n    };\n\n  }\n\n\n  /**\n   * Receives an OAuth2 token using 'authorization_code' grant\n   */\n  async getToken(params: GetTokenParams): Promise<OAuth2Token> {\n\n    const body:AuthorizationCodeRequest = {\n      grant_type: 'authorization_code',\n      code: params.code,\n      redirect_uri: params.redirectUri,\n      code_verifier: params.codeVerifier,\n      resource: params.resource,\n    };\n    return this.client.tokenResponseToOAuth2Token(this.client.request('tokenEndpoint', body));\n\n  }\n\n\n}\n\nexport async function generateCodeVerifier(): Promise<string> {\n\n  const webCrypto = await getWebCrypto();\n  const arr = new Uint8Array(32);\n  webCrypto.getRandomValues(arr);\n  return base64Url(arr);\n\n}\n\nexport async function getCodeChallenge(codeVerifier: string): Promise<['plain' | 'S256', string]> {\n\n  const webCrypto = await getWebCrypto();\n  return ['S256', base64Url(await webCrypto.subtle.digest('SHA-256', stringToBuffer(codeVerifier)))];\n}\n\nasync function getWebCrypto(): Promise<typeof window.crypto> {\n\n  // Browsers\n  if ((typeof window !== 'undefined' && window.crypto)) {\n    if (!window.crypto.subtle?.digest) {\n      throw new Error(\n        \"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\"\n      );\n    }\n    return window.crypto;\n  }\n  // Web workers possibly\n  if ((typeof self !== 'undefined' && self.crypto)) {\n    return self.crypto;\n  }\n  // Node\n  // eslint-disable-next-line @typescript-eslint/no-var-requires\n  const crypto = await import('crypto');\n  return crypto.webcrypto as typeof window.crypto;\n\n}\n\nfunction stringToBuffer(input: string): ArrayBuffer {\n\n  const buf = new Uint8Array(input.length);\n  for(let i=0; i<input.length;i++) {\n    buf[i] = input.charCodeAt(i) & 0xFF;\n  }\n  return buf;\n\n}\n\nfunction base64Url(buf: ArrayBuffer) {\n  return (\n    btoa(String.fromCharCode(...new Uint8Array(buf)))\n      .replace(/\\+/g, '-')\n      .replace(/\\//g, '_')\n      .replace(/=+$/, '')\n  );\n}\n\n"
  },
  {
    "path": "src/client.ts",
    "content": "import type { OAuth2Token } from './token.ts';\nimport type {\n  AuthorizationCodeRequest,\n  ClientCredentialsRequest,\n  IntrospectionRequest,\n  IntrospectionResponse,\n  PasswordRequest,\n  OAuth2TokenTypeHint,\n  RefreshRequest,\n  RevocationRequest,\n  ServerMetadataResponse,\n  TokenResponse,\n} from './messages.ts';\nimport { OAuth2HttpError } from './error.ts';\nimport { OAuth2AuthorizationCodeClient } from './client/authorization-code.ts';\n\n\ntype ClientCredentialsParams = {\n  scope?: string[];\n  extraParams?: Record<string, string>;\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n}\n\ntype PasswordParams = {\n  username: string;\n  password: string;\n\n  scope?: string[];\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n\n}\n\n/**\n * Extra options that may be passed to refresh()\n */\ntype RefreshParams = {\n  scope?: string[];\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n\n}\n\nexport interface ClientSettings {\n\n  /**\n   * The hostname of the OAuth2 server.\n   * If provided, we'll attempt to discover all the other related endpoints.\n   *\n   * If this is not desired, just specify the other endpoints manually.\n   *\n   * This url will also be used as the base URL for all other urls. This lets\n   * you specify all the other urls as relative.\n   */\n  server?: string;\n\n  /**\n   * OAuth2 clientId\n   */\n  clientId: string;\n\n  /**\n   * OAuth2 clientSecret\n   *\n   * This is required when using the 'client_secret_basic' authenticationMethod\n   * for the client_credentials and password flows, but not authorization_code\n   * or implicit.\n   */\n  clientSecret?: string;\n\n  /**\n   * The /authorize endpoint.\n   *\n   * Required only for the browser-portion of the authorization_code flow.\n   */\n  authorizationEndpoint?: string;\n\n  /**\n   * The token endpoint.\n   *\n   * Required for most grant types and refreshing tokens.\n   */\n  tokenEndpoint?: string;\n\n  /**\n   * Introspection endpoint.\n   *\n   * Required for, well, introspecting tokens.\n   * If not provided we'll try to discover it, or otherwise default to /introspect\n   */\n  introspectionEndpoint?: string;\n\n  /**\n   * Revocation endpoint.\n   *\n   * Required for revoking tokens. Not supported by all servers.\n   * If not provided we'll try to discover it, or otherwise default to /revoke\n   */\n  revocationEndpoint?: string;\n\n  /**\n   * OAuth 2.0 Authorization Server Metadata endpoint or OpenID\n   * Connect Discovery 1.0 endpoint.\n   *\n   * If this endpoint is provided it can be used to automatically figure\n   * out all the other endpoints.\n   *\n   * Usually the URL for this is: https://server/.well-known/oauth-authorization-server\n   */\n  discoveryEndpoint?: string;\n\n  /**\n   * Fetch implementation to use.\n   *\n   * Set this if you wish to explicitly set the fetch implementation, e.g. to\n   * implement middlewares or set custom headers.\n   */\n  fetch?: typeof fetch;\n\n  /**\n   * Client authentication method that is used to authenticate\n   * when using the token endpoint.\n   *\n   * When 'client_secret_basic' is used, the client_id and client_secret are\n   * encoded in the Authorization header, as per RFC 6749 section 2.3.1. This\n   * uses the official encoding, which also percent-encodes special characters.\n   *\n   * Many popular servers don't expect this, despite being the standard. So we\n   * also support 'client_secret_basic_interop', which does not percent-encode\n   * special characters except \":\". This is 'interop' encoding is the default\n   * for this library to maximize compatibility.\n   *\n   * In the future, we will switch this to 'client_secret_post', which has fewer\n   * interopability issues. This setting causes the client to provide the\n   * client_id and secret in the POST body.\n   *\n   * The default value is 'client_secret_basic' if not provided.\n   */\n  authenticationMethod?: 'client_secret_basic' | 'client_secret_post' | 'client_secret_basic_interop';\n\n}\n\n\ntype OAuth2Endpoint = 'tokenEndpoint' | 'authorizationEndpoint' | 'discoveryEndpoint' | 'introspectionEndpoint' | 'revocationEndpoint';\n\nexport class OAuth2Client {\n\n  settings: ClientSettings;\n\n  constructor(clientSettings: ClientSettings) {\n\n    if (!clientSettings?.fetch) {\n      clientSettings.fetch = fetch.bind(globalThis);\n    }\n    this.settings = clientSettings;\n\n  }\n\n  /**\n   * Refreshes an existing token, and returns a new one.\n   */\n  async refreshToken(token: OAuth2Token, params?: RefreshParams): Promise<OAuth2Token> {\n\n    if (!token.refreshToken) {\n      throw new Error('This token didn\\'t have a refreshToken. It\\'s not possible to refresh this');\n    }\n\n    const body: RefreshRequest = {\n      grant_type: 'refresh_token',\n      refresh_token: token.refreshToken,\n    };\n    if (!this.settings.clientSecret) {\n      // If there's no secret, send the clientId in the body.\n      body.client_id = this.settings.clientId;\n    }\n\n    if (params?.scope) body.scope = params.scope.join(' ');\n    if (params?.resource) body.resource = params.resource;\n\n    const newToken = await this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));\n    if (!newToken.refreshToken && token.refreshToken) {\n      // Reuse old refresh token if we didn't get a new one.\n      newToken.refreshToken = token.refreshToken;\n    }\n    return newToken;\n\n  }\n\n  /**\n   * Retrieves an OAuth2 token using the client_credentials grant.\n   */\n  async clientCredentials(params?: ClientCredentialsParams): Promise<OAuth2Token> {\n\n    const disallowed = ['client_id', 'client_secret', 'grant_type', 'scope'];\n\n    if (params?.extraParams && Object.keys(params.extraParams).filter((key) => disallowed.includes(key)).length > 0) {\n      throw new Error(`The following extraParams are disallowed: '${disallowed.join(\"', '\")}'`);\n    }\n\n    const body: ClientCredentialsRequest = {\n      grant_type: 'client_credentials',\n      scope: params?.scope?.join(' '),\n      resource: params?.resource,\n      ...params?.extraParams\n    };\n\n    if (!this.settings.clientSecret) {\n      throw new Error('A clientSecret must be provided to use client_credentials');\n    }\n\n    return this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));\n\n  }\n\n  /**\n   * Retrieves an OAuth2 token using the 'password' grant'.\n   */\n  async password(params: PasswordParams): Promise<OAuth2Token> {\n\n    const body: PasswordRequest = {\n      grant_type: 'password',\n      ...params,\n      scope: params.scope?.join(' '),\n    };\n    return this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body));\n\n  }\n\n  /**\n   * Returns the helper object for the `authorization_code` grant.\n   */\n  get authorizationCode(): OAuth2AuthorizationCodeClient {\n\n    return new OAuth2AuthorizationCodeClient(\n      this,\n    );\n\n  }\n\n  /**\n   * Introspect a token\n   *\n   * This will give information about the validity, owner, which client\n   * created the token and more.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc7662\n   */\n  async introspect(token: OAuth2Token): Promise<IntrospectionResponse> {\n\n    const body: IntrospectionRequest = {\n      token: token.accessToken,\n      token_type_hint: 'access_token',\n    };\n    return this.request('introspectionEndpoint', body);\n\n  }\n\n  /**\n   * Revoke a token\n   *\n   * This will revoke a token, provided that the server supports this feature.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc7009\n   */\n  async revoke(token: OAuth2Token, tokenTypeHint: OAuth2TokenTypeHint = 'access_token'): Promise<void> {\n    let tokenValue = token.accessToken;\n    if (tokenTypeHint === 'refresh_token') {\n      tokenValue = token.refreshToken!;\n    }\n\n    const body: RevocationRequest = {\n      token: tokenValue,\n      token_type_hint: tokenTypeHint,\n    };\n    return this.request('revocationEndpoint', body);\n\n  }\n\n  /**\n   * Returns a url for an OAuth2 endpoint.\n   *\n   * Potentially fetches a discovery document to get it.\n   */\n  async getEndpoint(endpoint: OAuth2Endpoint): Promise<string> {\n\n    if (this.settings[endpoint] !== undefined) {\n      return resolve(this.settings[endpoint] as string, this.settings.server);\n    }\n\n    if (endpoint !== 'discoveryEndpoint') {\n      // This condition prevents infinite loops.\n      await this.discover();\n      if (this.settings[endpoint] !== undefined) {\n        return resolve(this.settings[endpoint] as string, this.settings.server);\n      }\n    }\n\n    // If we got here it means we need to 'guess' the endpoint.\n    if (!this.settings.server) {\n      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.`);\n    }\n\n    switch (endpoint) {\n      case 'authorizationEndpoint':\n        return resolve('/authorize', this.settings.server);\n      case 'tokenEndpoint':\n        return resolve('/token', this.settings.server);\n      case 'discoveryEndpoint':\n        return resolve('/.well-known/oauth-authorization-server', this.settings.server);\n      case 'introspectionEndpoint':\n        return resolve('/introspect', this.settings.server);\n      case 'revocationEndpoint':\n        return resolve('/revoke', this.settings.server);\n    }\n\n  }\n\n  private discoveryPromise: Promise<void> | undefined;\n  private serverMetadata: ServerMetadataResponse | null = null;\n\n  private discover(): Promise<void> {\n    // Never discover twice\n    if (this.discoveryPromise === undefined) {\n      this.discoveryPromise = this.doDiscover();\n    }\n    return this.discoveryPromise;\n  }\n\n  /**\n   * Fetches the OAuth2 discovery document\n   *\n   * Should not call this directly, call `discover()` instead\n   */\n  private async doDiscover(): Promise<void> {\n\n    let discoverUrl;\n    try {\n      discoverUrl = await this.getEndpoint('discoveryEndpoint');\n    } catch (_err) {\n      console.warn('[oauth2] OAuth2 discovery endpoint could not be determined. Either specify the \"server\" or \"discoveryEndpoint');\n      return;\n    }\n    const resp = await this.settings.fetch!(discoverUrl, { headers: { Accept: 'application/json' }});\n\n    if (!resp.ok) return;\n    if (!resp.headers.get('Content-Type')?.startsWith('application/json')) {\n      console.warn('[oauth2] OAuth2 discovery endpoint was not a JSON response. Response is ignored');\n      return;\n    }\n    this.serverMetadata = await resp.json();\n\n    const urlMap = [\n      ['authorization_endpoint', 'authorizationEndpoint'],\n      ['token_endpoint', 'tokenEndpoint'],\n      ['introspection_endpoint', 'introspectionEndpoint'],\n      ['revocation_endpoint', 'revocationEndpoint'],\n    ] as const;\n\n    if (this.serverMetadata === null) return;\n\n    for (const [property, setting] of urlMap) {\n      if (!this.serverMetadata[property]) continue;\n      this.settings[setting] = resolve(this.serverMetadata[property]!, discoverUrl);\n    }\n\n    if (\n      this.serverMetadata.token_endpoint_auth_methods_supported\n      && !this.settings.authenticationMethod\n    ) {\n      for(const method of this.serverMetadata.token_endpoint_auth_methods_supported) {\n        if (method === 'client_secret_basic' || method === 'client_secret_post') {\n          this.settings.authenticationMethod = method;\n          break;\n        }\n      }\n    }\n\n  }\n\n  /**\n   * Does a HTTP request on the 'token' endpoint.\n   */\n  async request(endpoint: 'tokenEndpoint', body: RefreshRequest | ClientCredentialsRequest | PasswordRequest | AuthorizationCodeRequest): Promise<TokenResponse>;\n  async request(endpoint: 'introspectionEndpoint', body: IntrospectionRequest): Promise<IntrospectionResponse>;\n  async request(endpoint: 'revocationEndpoint', body: RevocationRequest): Promise<void>;\n  async request(endpoint: OAuth2Endpoint, body: Record<string, any>): Promise<unknown> {\n\n    const uri = await this.getEndpoint(endpoint);\n\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/x-www-form-urlencoded',\n      // Although it shouldn't be needed, Github OAUth2 will return JSON\n      // unless this is set.\n      'Accept': 'application/json',\n    };\n\n    let authMethod = this.settings.authenticationMethod;\n\n    if (!this.settings.clientSecret) {\n      // Basic auth should only be used when there's a client_secret, for\n      // non-confidential clients we may only have a client_id, which\n      // always gets added to the body.\n      authMethod = 'client_secret_post';\n    }\n    if (!authMethod) {\n      // If we got here, it means no preference was provided by anything,\n      // and we have a secret. In this case its preferred to embed\n      // authentication in the Authorization header.\n      authMethod = 'client_secret_basic_interop';\n    }\n\n    switch(authMethod) {\n      case 'client_secret_basic' :\n        // Per RFC 6749 section 2.3.1, the client_id and client_secret need\n        // to be encoded using application/x-www-form-urlencoded for the\n        // basic auth.\n        headers.Authorization = 'Basic ' +\n          btoa(legacyFormUrlEncode(this.settings.clientId) + ':' + legacyFormUrlEncode(this.settings.clientSecret!));\n        break;\n      case 'client_secret_basic_interop' :\n        // A more relaxed encoding that's more compatible with popular servers.\n        headers.Authorization = 'Basic ' +\n          btoa(this.settings.clientId.replace(/:/g, '%3A') + ':' + this.settings.clientSecret!.replace(/:/g, '%3A'));\n        break;\n      case 'client_secret_post' :\n        body.client_id = this.settings.clientId;\n        if (this.settings.clientSecret) {\n          body.client_secret = this.settings.clientSecret;\n        }\n        break;\n      default:\n        throw new Error('Authentication method not yet supported:' + authMethod + '. Open a feature request if you want this!');\n    }\n\n    const resp = await this.settings.fetch!(uri, {\n      method: 'POST',\n      body: generateQueryString(body),\n      headers,\n    });\n\n    let responseBody;\n    if (resp.status !== 204 && resp.headers.has('Content-Type') && resp.headers.get('Content-Type')!.match(/^application\\/(.*\\+)?json/)) {\n      responseBody = await resp.json();\n    }\n    if (resp.ok) {\n      return responseBody;\n    }\n\n    let errorMessage;\n    let oauth2Code;\n\n    if (responseBody?.error) {\n      // This is likely an OAUth2-formatted error\n      errorMessage = 'OAuth2 error ' + responseBody.error + '.';\n      if (responseBody.error_description) {\n        errorMessage += ' ' + responseBody.error_description;\n      }\n      oauth2Code = responseBody.error;\n\n    } else {\n      errorMessage = 'HTTP Error ' + resp.status + ' ' + resp.statusText;\n      if (resp.status === 401 && this.settings.clientSecret) {\n        errorMessage += '. It\\'s likely that the clientId and/or clientSecret was incorrect';\n      }\n      oauth2Code = null;\n    }\n    throw new OAuth2HttpError(errorMessage, oauth2Code, resp, responseBody);\n  }\n\n  /**\n   * Converts the JSON response body from the token endpoint to an OAuth2Token type.\n   */\n  async tokenResponseToOAuth2Token(resp: Promise<TokenResponse>): Promise<OAuth2Token> {\n\n    const body = await resp;\n\n    if (!body?.access_token) {\n      console.warn('Invalid OAuth2 Token Response: ', body);\n      throw new TypeError('We received an invalid token response from an OAuth2 server.');\n    }\n\n    const {\n      access_token,\n      refresh_token,\n      expires_in,\n      id_token,\n      scope,\n      token_type,\n      ...extraParams\n    } = body;\n\n    const result: OAuth2Token = {\n      accessToken: access_token,\n      expiresAt: expires_in ? Date.now() + (expires_in * 1000) : null,\n      refreshToken: refresh_token ?? null,\n    };\n    if (id_token) {\n      result.idToken = id_token;\n    }\n    if (scope) {\n      result.scope = scope.split(' ');\n    }\n    if (Object.keys(extraParams).length > 0) {\n      result.extraParams = extraParams;\n    }\n    return result;\n\n  }\n\n}\n\nfunction resolve(uri: string, base?: string): string {\n\n  return new URL(uri, base).toString();\n\n}\n\n/**\n * Generates a query string.\n *\n * If a value is undefined, it will be ignored.\n * If a value is an array, it will add the parameter multiple times for each array value.\n */\nexport function generateQueryString(params: Record<string, undefined | number | string | string[]>): string {\n\n  const query = new URLSearchParams();\n  for (const [k, v] of Object.entries(params)) {\n    if (Array.isArray(v)) {\n      for(const vItem of v) query.append(k, vItem);\n    } else if (v !== undefined) query.set(k, v.toString());\n  }\n  return query.toString();\n\n}\n\n/**\n * Encodes string according to the most strict interpretation of RFC 6749 Appendix B.\n *\n * All non-alphanumeric characters are percent encoded, with exception of space which\n * is replaced with '+'.\n */\nexport function legacyFormUrlEncode(value: string): string {\n  return encodeURIComponent(value)\n    .replace(/%20/g, '+')\n    .replace(/[-_.!~*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n"
  },
  {
    "path": "src/error.ts",
    "content": "import { type OAuth2ErrorCode } from './messages.ts';\n\n/**\n * An error class for any error the server emits.\n *\n * The 'oauth2Code' property will have the oauth2 error type,\n * such as:\n * - invalid_request\n * - invalid_client\n * - invalid_grant\n * - unauthorized_client\n * - unsupported_grant_type\n * - invalid_scope\n */\nexport class OAuth2Error extends Error {\n\n  oauth2Code: OAuth2ErrorCode|string;\n\n  constructor(message: OAuth2ErrorCode|string, oauth2Code: OAuth2ErrorCode) {\n\n    super(message);\n    this.oauth2Code = oauth2Code;\n\n  }\n\n}\n\n/**\n * A OAuth2 error that was emitted as a HTTP error\n *\n * The 'code' property will have the oauth2 error type,\n * such as:\n * - invalid_request\n * - invalid_client\n * - invalid_grant\n * - unauthorized_client\n * - unsupported_grant_type\n * - invalid_scope\n *\n * This Error also gives you access to the HTTP status code and response body.\n */\nexport class OAuth2HttpError extends OAuth2Error {\n\n  httpCode: number;\n\n  response: Response;\n  parsedBody: Record<string, any>;\n\n  constructor(message: string, oauth2Code: OAuth2ErrorCode, response: Response, parsedBody: Record<string, any>) {\n\n    super(message, oauth2Code);\n\n    this.httpCode = response.status;\n    this.response = response;\n    this.parsedBody = parsedBody;\n\n  }\n\n}\n"
  },
  {
    "path": "src/fetch-wrapper.ts",
    "content": "import type { OAuth2Token } from './token.ts';\nimport { OAuth2Client } from './client.ts';\n\ntype FetchMiddleware = (request: Request, next: (request: Request) => Promise<Response>) => Promise<Response>;\n\ntype OAuth2FetchOptions = {\n\n  /**\n   * Reference to OAuth2 client.\n   */\n  client: OAuth2Client;\n\n  /**\n   * You are responsible for implementing this function.\n   * it's purpose is to supply the 'initial' oauth2 token.\n   *\n   * This function may be async. Return `null` to fail the process.\n   */\n  getNewToken(): OAuth2Token | null | Promise<OAuth2Token | null>;\n\n  /**\n   * If set, will be called if authentication fatally failed.\n   */\n  onError?: (err: Error) => void;\n\n  /**\n   * This function is called whenever the active token changes. Using this is\n   * optional, but it may be used to (for example) put the token in off-line\n   * storage for later usage.\n   */\n  storeToken?: (token: OAuth2Token) => void;\n\n  /**\n   * Also an optional feature. Implement this if you want the wrapper to try a\n   * stored token before attempting a full re-authentication.\n   *\n   * This function may be async. Return null if there was no token.\n   */\n  getStoredToken?: () => OAuth2Token | null | Promise<OAuth2Token | null>;\n\n  /**\n   * Whether to automatically schedule token refresh.\n   *\n   * Certain execution environments, e.g. React Native, do not handle scheduled\n   * tasks with setTimeout() in a graceful or predictable fashion. The default\n   * behavior is to schedule refresh. Set this to false to disable scheduling.\n   */\n  scheduleRefresh?: boolean;\n\n}\n\nexport class OAuth2Fetch {\n\n  private options: OAuth2FetchOptions;\n\n  /**\n   * Current active token (if any)\n   */\n  private token: OAuth2Token | null = null;\n\n  /**\n   * If the user had a storedToken, the process to fetch it\n   * may be async. We keep track of this process in this\n   * promise, so it may be awaited to avoid race conditions.\n   *\n   * As soon as this promise resolves, this property gets nulled.\n   */\n  private activeGetStoredToken: null | Promise<void> = null;\n\n  constructor(options: OAuth2FetchOptions) {\n\n    if (options?.scheduleRefresh === undefined) {\n      options.scheduleRefresh = true;\n    }\n    this.options = options;\n    if (options.getStoredToken) {\n      this.activeGetStoredToken = (async () => {\n        this.token = await options.getStoredToken!();\n        this.activeGetStoredToken = null;\n      })();\n    }\n    this.scheduleRefresh();\n\n  }\n\n  /**\n   * Does a fetch request and adds a Bearer / access token.\n   *\n   * If the access token is not known, this function attempts to fetch it\n   * first. If the access token is almost expiring, this function might attempt\n   * to refresh it.\n   */\n  async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {\n\n    // input might be a string or a Request object, we want to make sure this\n    // is always a fully-formed Request object.\n    const request = new Request(input, init);\n\n    return this.mw()(\n      request,\n      req => fetch(req)\n    );\n\n  }\n\n  /**\n   * This function allows the fetch-mw to be called as more traditional\n   * middleware.\n   *\n   * This function returns a middleware function with the signature\n   *    (request, next): Response\n   */\n  mw(): FetchMiddleware {\n\n    return async (request, next) => {\n\n      const accessToken = await this.getAccessToken();\n\n      // Make a clone. We need to clone if we need to retry the request later.\n      let authenticatedRequest = request.clone();\n      authenticatedRequest.headers.set('Authorization', 'Bearer '  + accessToken);\n      let response = await next(authenticatedRequest);\n\n      if (!response.ok && response.status === 401) {\n\n        const newToken = await this.refreshToken();\n\n        authenticatedRequest = request.clone();\n        authenticatedRequest.headers.set('Authorization', 'Bearer '  + newToken.accessToken);\n        response = await next(authenticatedRequest);\n\n      }\n      return response;\n    };\n\n  }\n\n  /**\n   * Returns current token information.\n   *\n   * There result object will have:\n   *   * accessToken\n   *   * expiresAt - when the token expires, or null.\n   *   * refreshToken - may be null\n   *\n   * This function will attempt to automatically refresh if stale.\n   */\n  async getToken(): Promise<OAuth2Token> {\n\n    if (this.token && (this.token.expiresAt === null || this.token.expiresAt > Date.now())) {\n\n      // The current token is still valid\n      return this.token;\n\n    }\n\n    return this.refreshToken();\n\n  }\n\n  /**\n   * Returns an access token.\n   *\n   * If the current access token is not known, it will attempt to fetch it.\n   * If the access token is expiring, it will attempt to refresh it.\n   */\n  async getAccessToken(): Promise<string> {\n\n    // Ensure getStoredToken finished.\n    await this.activeGetStoredToken;\n\n    const token = await this.getToken();\n    return token.accessToken;\n\n  }\n\n  /**\n   * Keeping track of an active refreshToken operation.\n   *\n   * This will allow us to ensure only 1 such operation happens at any\n   * given time.\n   */\n  private activeRefresh: Promise<OAuth2Token> | null = null;\n\n  /**\n   * Forces an access token refresh\n   */\n  async refreshToken(): Promise<OAuth2Token> {\n\n    if (this.activeRefresh) {\n      // If we are currently already doing this operation,\n      // make sure we don't do it twice in parallel.\n      return this.activeRefresh;\n    }\n\n    const oldToken = this.token;\n    this.activeRefresh = (async() => {\n\n      let newToken: OAuth2Token|null = null;\n\n      try {\n        if (oldToken?.refreshToken) {\n          // We had a refresh token, lets see if we can use it!\n          newToken = await this.options.client.refreshToken(oldToken);\n        }\n      } catch (_err) {\n        console.warn('[oauth2] refresh token not accepted, we\\'ll try reauthenticating');\n      }\n\n      if (!newToken) {\n        newToken = await this.options.getNewToken();\n      }\n\n      if (!newToken) {\n        const err = new Error('Unable to obtain OAuth2 tokens, a full reauth may be needed');\n        this.options.onError?.(err);\n        throw err;\n      }\n      return newToken;\n\n    })();\n\n    try {\n      const token = await this.activeRefresh;\n      this.token = token;\n      this.options.storeToken?.(token);\n      this.scheduleRefresh();\n      return token;\n    } catch (err: any) {\n      if (this.options.onError) {\n        this.options.onError(err);\n      }\n      throw err;\n    } finally {\n      // Make sure we clear the current refresh operation.\n      this.activeRefresh = null;\n    }\n\n  }\n\n  /**\n   * Timer trigger for the next automated refresh\n   */\n  private refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n  private scheduleRefresh() {\n    if (!this.options.scheduleRefresh) {\n      return;\n    }\n    if (this.refreshTimer) {\n      clearTimeout(this.refreshTimer);\n      this.refreshTimer = null;\n    }\n\n    if (!this.token?.expiresAt || !this.token.refreshToken) {\n      // If we don't know when the token expires, or don't have a refresh_token, don't bother.\n      return;\n    }\n\n    const expiresIn = this.token.expiresAt - Date.now();\n\n    // We only schedule this event if it happens more than 2 minutes in the future.\n    if (expiresIn < 120*1000) {\n      return;\n    }\n\n    // Schedule 1 minute before expiry\n    this.refreshTimer = setTimeout(async () => {\n      try {\n        await this.refreshToken();\n      } catch (err) {\n        // eslint-disable-next-line no-console\n        console.error('[fetch-mw-oauth2] error while doing a background OAuth2 auto-refresh', err);\n      }\n    }, expiresIn - 60*1000);\n\n  }\n\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export { OAuth2Client } from './client.ts';\nexport { OAuth2AuthorizationCodeClient, generateCodeVerifier } from './client/authorization-code.ts';\nexport { OAuth2Fetch } from './fetch-wrapper.ts';\nexport { OAuth2Error, OAuth2HttpError } from './error.ts';\n\nexport type { IntrospectionResponse } from './messages.ts';\nexport type { OAuth2Token } from './token.ts';\n"
  },
  {
    "path": "src/messages.ts",
    "content": "/**\n * refresh_token request body\n */\nexport type RefreshRequest = {\n  grant_type: 'refresh_token';\n  refresh_token: string;\n\n  client_id?: string;\n  scope?: string;\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n}\n\n/**\n * client_credentials request body\n */\nexport type ClientCredentialsRequest = {\n  grant_type: 'client_credentials';\n  scope?: string;\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n\n  [key: string]: string | undefined | string[];\n}\n\n/**\n * password grant_type request body\n */\nexport type PasswordRequest = {\n  grant_type: 'password';\n  username: string;\n  password: string;\n  scope?: string;\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n}\n\nexport type AuthorizationCodeRequest = {\n  grant_type: 'authorization_code';\n  code: string;\n  redirect_uri: string;\n  code_verifier: string|undefined;\n\n  /**\n   * The resource  the client intends to access.\n   *\n   * @see https://datatracker.ietf.org/doc/html/rfc8707\n   */\n  resource?: string | string[];\n}\n\n/**\n * Response from the /token endpoint\n */\nexport type TokenResponse = {\n  /**\n   * The OAuth 2 access token.\n   */\n  access_token: string;\n\n  /**\n   * The type of token, which is always \"Bearer\".\n   */\n  token_type: string;\n\n  /**\n   * The lifetime in seconds of the access token.\n   */\n  expires_in?: number;\n\n  /**\n   * The refresh token, which can be used to get a new access token after the current one expires.\n   */\n  refresh_token?: string;\n\n  /**\n   * List of comma-separated scopes that the access token is valid for.\n   */\n  scope?: string;\n\n  /**\n   * The OpenID Connect id_token, which is a JWT encoded value containing\n   * information about the authenticated user.\n   */\n  id_token?: string;\n}\n\ntype OAuth2ResponseType = 'code' | 'token';\ntype OAuth2ResponseMode = 'query' | 'fragment';\ntype 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';\ntype OAuth2AuthMethod = 'none' | 'client_secret_basic' | 'client_secret_post' | 'client_secret_jwt' | 'private_key_jwt' | 'tls_client_auth' | 'self_signed_tls_client_auth';\ntype OAuth2CodeChallengeMethod = 'S256' | 'plain';\n\nexport type OAuth2TokenTypeHint = 'access_token' | 'refresh_token';\n\n/**\n * Response from /.well-known/oauth-authorization-server\n *\n * https://datatracker.ietf.org/doc/html/rfc8414\n */\nexport type ServerMetadataResponse = {\n\n  /**\n   * The authorization server's issuer identifier, which is a URL that uses\n   * the \"https\" scheme and has no query or fragment.\n   */\n  issuer: string;\n\n  /**\n   * URL of the authorization server's authorization endpoint.\n   */\n  authorization_endpoint:string;\n\n  /**\n   * URL of the authorization server's token endpoint.\n   */\n  token_endpoint: string;\n\n  /**\n   * URL of the authorization server's JWK Set document\n   */\n  jwks_uri?: string;\n\n  /**\n   * URL of the authorization server's OAuth 2.0 Dynamic Client Registration\n   * endpoint.\n   */\n  registration_endpoint?: string;\n\n  /**\n   * List of supported scopes for this server\n   */\n  scopes_supported?: string[];\n\n  /**\n   * List of supported response types for the authorization endpoint.\n   *\n   * If 'code' appears here it implies authorization_code support,\n   * 'token' implies support for implicit auth.\n   */\n  response_types_supported: OAuth2ResponseType[];\n\n  /**\n   * JSON array containing a list of the OAuth 2.0 \"response_mode\"\n   * values that this authorization server supports\n   */\n  response_modes_supported?: OAuth2ResponseMode[];\n\n  /**\n   * List of supported grant types by the server\n   */\n  grant_types_supported?: OAuth2GrantType[];\n\n  /**\n   * Supported auth methods on the token endpoint.\n   */\n  token_endpoint_auth_methods_supported?: OAuth2AuthMethod[];\n\n  /**\n   * JSON array containing a list of the JWS signing algorithms.\n   */\n  token_endpoint_auth_signing_alg_values_supported?: string[];\n\n  /**\n   * URL of a page containing human-readable information that developers might want or need to know when using the authorization server.\n   */\n  service_documentation?: string;\n\n  /**\n   * List of supported languages for the UI\n   */\n  ui_locales_supported?: string[];\n\n  /**\n   * URL that the authorization server provides to the person registering the\n   * client to read about the authorization server's requirements on how the\n   * client can use the data provided by the authorization server.\n   */\n  op_policy_uri?: string;\n\n  /**\n   * Link to terms of service\n   */\n  op_tos_uri?: string;\n\n  /**\n   * Url to servers revocation endpoint.\n   */\n  revocation_endpoint?: string;\n\n  /**\n   * Auth method that may be used on the revocation endpoint.\n   */\n  revocation_endpoint_auth_methods_supported?: OAuth2AuthMethod[];\n\n  /**\n   * JSON array containing a list of the JWS signing algorithms (\"alg\" values)\n   * supported by the revocation endpoint.\n   */\n  revocation_endpoint_auth_signing_alg_values_supported?: string[];\n\n  /**\n   * Url to introspection endpoint\n   */\n  introspection_endpoint?: string;\n\n  /**\n   * List of authentication methods supported on the introspection endpoint.\n   */\n  introspection_endpoint_auth_methods_supported?: OAuth2AuthMethod[];\n\n  /**\n   * List of JWS signing algorithms supported on the introspection endpoint.\n   */\n  introspection_endpoint_auth_signing_alg_values_supported?: string[];\n\n  /**\n   * List of support PCKE code challenge methods.\n   */\n  code_challenge_methods_supported?: OAuth2CodeChallengeMethod[];\n\n}\n\nexport type IntrospectionRequest = {\n  token: string;\n  token_type_hint?: OAuth2TokenTypeHint;\n};\n\n\nexport type IntrospectionResponse = {\n\n  /**\n   * Whether or not the token is still active.\n   */\n  active: boolean;\n\n  /**\n   * Space-separated list of scopes.\n   */\n  scope?: string;\n\n  /**\n   * client_id that requested the token.\n   */\n  client_id?: string;\n\n  /**\n   * Human-readable string of the resource-owner that requested the token.\n   */\n  username?: string;\n\n  /**\n   * Type of token\n   */\n  token_type?: string;\n\n  /**\n   * Unix timestamp of when this token expires.\n   */\n  exp?: number;\n\n  /**\n   * Unix timestamp of when the token was issued.\n   */\n  iat?: number;\n\n  /**\n   * Unix timestamp indicating when the token should not be used before.\n   */\n  nbf?: number;\n\n  /**\n   * Subject of the token. Usually a machine-readable identifier of the\n   * resource owner/user.\n   */\n  sub?: string;\n\n  /**\n   * String representing the audience of the token.\n   */\n  aud?: string;\n\n  /**\n   * Issuer of the token.\n   */\n  iss?: string;\n\n  /**\n   * String identifier of the token.\n   */\n  jti?: string;\n\n}\n\n/**\n * Revocaton request.\n *\n * https://datatracker.ietf.org/doc/html/rfc7009#section-2.1\n */\nexport type RevocationRequest = {\n  token: string;\n  token_type_hint?: OAuth2TokenTypeHint;\n}\n\nexport type OAuth2ErrorCode =\n  | 'invalid_request'\n  | 'invalid_client'\n  | 'invalid_grant'\n  | 'unauthorized_client'\n  | 'unsupported_grant_type'\n  | 'invalid_scope'\n\n  /**\n   * RFC 8707\n   */\n  | 'invalid_target';\n"
  },
  {
    "path": "src/token.ts",
    "content": "/**\n * Token information\n */\nexport type OAuth2Token = {\n\n  /**\n   * OAuth2 Access Token\n   */\n  accessToken: string;\n\n  /**\n   * When the Access Token expires.\n   *\n   * This is expressed as a unix timestamp in milliseconds.\n   */\n  expiresAt: number | null;\n\n  /**\n   * OAuth2 refresh token\n   */\n  refreshToken: string | null;\n\n  /**\n   * OpenID Connect ID Token\n   */\n  idToken?: string;\n\n  /**\n   * List of scopes that the access token is valid for.\n   * (May be omitted if identical to the requested scope)\n   */\n  scope?: string[];\n\n  /**\n   * Additional tokens properties returned by the OAuth2 server.\n   */\n  extraParams?: Record<string, any>;\n};\n"
  },
  {
    "path": "test/authorization-code.ts",
    "content": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../src/index.ts';\nimport { after, describe, it } from 'node:test';\n\n// Example directly taken from https://datatracker.ietf.org/doc/html/rfc7636\nconst codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';\nconst codeChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';\n\ndescribe('authorization-code', () => {\n  let server: ReturnType<typeof testServer>;\n\n  after(async () => {\n    if (server) {\n      await server.close();\n    }\n  });\n\n  describe('Authorization endpoint redirect', () => {\n    it('should generate correct urls for the authorization endpoint', async () => {\n      server = testServer();\n      const client = new OAuth2Client({\n        server: server.url,\n        authorizationEndpoint: '/authorize',\n        clientId: 'test-client-id',\n      });\n\n      const redirectUri = 'http://my-app.example/redirect';\n\n      const params = new URLSearchParams({\n        client_id: 'test-client-id',\n        response_type: 'code',\n        redirect_uri: redirectUri,\n        scope: 'a b',\n      });\n\n      assert.equal(\n        await client.authorizationCode.getAuthorizeUri({\n          redirectUri,\n          scope: ['a', 'b'],\n        }),\n        server.url + '/authorize?' + params.toString()\n      );\n    });\n    it('should support extraparams', async () => {\n      server = testServer();\n      const client = new OAuth2Client({\n        server: server.url,\n        authorizationEndpoint: '/authorize',\n        clientId: 'test-client-id',\n      });\n\n      const redirectUri = 'http://my-app.example/redirect';\n\n      const params = new URLSearchParams({\n        client_id: 'test-client-id',\n        response_type: 'code',\n        redirect_uri: redirectUri,\n        scope: 'a b',\n        foo: 'bar',\n      });\n\n      assert.equal(\n        await client.authorizationCode.getAuthorizeUri({\n          redirectUri,\n          scope: ['a', 'b'],\n          extraParams: {\n            foo: 'bar',\n          },\n        }),\n        server.url + '/authorize?' + params.toString()\n      );\n    });\n    it('should throw error when user rewrote params by extraparams', async () => {\n      server = testServer();\n      const client = new OAuth2Client({\n        server: server.url,\n        authorizationEndpoint: '/authorize',\n        clientId: 'test-client-id',\n      });\n\n      const redirectUri = 'http://my-app.example/redirect';\n\n      const params = {\n        redirectUri,\n        scope: ['a', 'b'],\n        state: 'some-state',\n      };\n\n      const extraParams = {\n        foo: 'bar',\n        scope: 'accidentally rewrote core parameter',\n      };\n\n      try {\n        await client.authorizationCode.getAuthorizeUri({\n          ...params,\n          extraParams,\n        });\n      } catch (error: any) {\n        assert.match(error.message, /Property in extraParams/);\n        return;\n      }\n\n      assert.fail('Should have thrown');\n    });\n    it('should support PKCE', async () => {\n      server = testServer();\n      const client = new OAuth2Client({\n        server: server.url,\n        authorizationEndpoint: '/authorize',\n        clientId: 'test-client-id',\n      });\n\n      const redirectUri = 'http://my-app.example/redirect';\n\n      const params = new URLSearchParams({\n        client_id: 'test-client-id',\n        response_type: 'code',\n        redirect_uri: redirectUri,\n        code_challenge_method: 'S256',\n        code_challenge: codeChallenge,\n      });\n\n      assert.equal(\n        await client.authorizationCode.getAuthorizeUri({\n          redirectUri,\n          codeVerifier,\n        }),\n        server.url + '/authorize?' + params.toString()\n      );\n    });\n    it('should support the resource parameter', async () => {\n      server = testServer();\n      const client = new OAuth2Client({\n        server: server.url,\n        authorizationEndpoint: '/authorize',\n        clientId: 'test-client-id',\n      });\n\n      const redirectUri = 'http://my-app.example/redirect';\n      const resource = ['https://example/foo1', 'https://example/foo2'];\n      const params = new URLSearchParams({\n        client_id: 'test-client-id',\n        response_type: 'code',\n        redirect_uri: redirectUri,\n      });\n      for (const r of resource) params.append('resource', r);\n\n      assert.equal(\n        await client.authorizationCode.getAuthorizeUri({\n          redirectUri,\n          resource,\n        }),\n        server.url + '/authorize?' + params.toString()\n      );\n    });\n  });\n\n  describe('Token endpoint calls', () => {\n    it('should send requests to the token endpoint', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'test-client-id',\n      });\n\n      const result = await client.authorizationCode.getToken({\n        code: 'code_000',\n        redirectUri: 'http://example/redirect',\n      });\n\n      assert.equal(result.accessToken, 'access_token_000');\n      assert.equal(result.refreshToken, 'refresh_token_000');\n      assert.deepEqual(result.scope, ['foo', 'bar']);\n      assert.deepEqual(result.extraParams, {\n        foo: 'bar',\n      });\n      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n      const request = server.lastRequest();\n      assert.equal(request.headers.get('Authorization'), null);\n      assert.equal(request.headers.get('Accept'), 'application/json');\n\n      assert.deepEqual(request.body, {\n        client_id: 'test-client-id',\n        grant_type: 'authorization_code',\n        code: 'code_000',\n        redirect_uri: 'http://example/redirect',\n      });\n    });\n\n    it('should send client_id and client_secret in the Authorization header if secret was specified', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'testClientId',\n        clientSecret: 'testClientSecret',\n      });\n\n      const result = await client.authorizationCode.getToken({\n        code: 'code_000',\n        redirectUri: 'http://example/redirect',\n      });\n\n      assert.equal(result.accessToken, 'access_token_000');\n      assert.equal(result.refreshToken, 'refresh_token_000');\n      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n      const request = server.lastRequest();\n      assert.equal(\n        request.headers.get('Authorization'),\n        'Basic ' + btoa('testClientId:testClientSecret')\n      );\n      assert.equal(request.headers.get('Accept'), 'application/json');\n\n      assert.deepEqual(request.body, {\n        grant_type: 'authorization_code',\n        code: 'code_000',\n        redirect_uri: 'http://example/redirect',\n      });\n    });\n\n    it('should should support PKCE', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'test-client-id',\n      });\n\n      const result = await client.authorizationCode.getToken({\n        code: 'code_000',\n        redirectUri: 'http://example/redirect',\n        codeVerifier,\n      });\n\n      assert.equal(result.accessToken, 'access_token_000');\n      assert.equal(result.refreshToken, 'refresh_token_000');\n      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n      const request = server.lastRequest();\n      assert.equal(request.headers.get('Authorization'), null);\n      assert.equal(request.headers.get('Accept'), 'application/json');\n\n      assert.deepEqual(request.body, {\n        client_id: 'test-client-id',\n        grant_type: 'authorization_code',\n        code: 'code_000',\n        code_verifier: codeVerifier,\n        redirect_uri: 'http://example/redirect',\n      });\n    });\n    it('should not use Basic Auth if no secret is provided, even if client_secret_basic is set.', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'test-client-id',\n        authenticationMethod: 'client_secret_basic',\n      });\n\n      const result = await client.authorizationCode.getToken({\n        code: 'code_000',\n        redirectUri: 'http://example/redirect',\n      });\n\n      assert.equal(result.accessToken, 'access_token_000');\n      assert.equal(result.refreshToken, 'refresh_token_000');\n      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n      const request = server.lastRequest();\n      assert.equal(request.headers.get('Authorization'), null);\n      assert.equal(request.headers.get('Accept'), 'application/json');\n\n      assert.deepEqual(request.body, {\n        client_id: 'test-client-id',\n        grant_type: 'authorization_code',\n        code: 'code_000',\n        redirect_uri: 'http://example/redirect',\n      });\n    });\n\n    it('should support the resource parameter', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'test-client-id',\n      });\n      const resource = ['https://example/foo1', 'https://example/foo2'];\n\n      const result = await client.authorizationCode.getToken({\n        code: 'code_000',\n        redirectUri: 'http://example/redirect',\n        resource,\n      });\n\n      assert.equal(result.accessToken, 'access_token_000');\n      assert.equal(result.refreshToken, 'refresh_token_000');\n      assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n      assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n      const request = server.lastRequest();\n      assert.equal(request.headers.get('Authorization'), null);\n      assert.equal(request.headers.get('Accept'), 'application/json');\n\n      assert.deepEqual(request.body, {\n        client_id: 'test-client-id',\n        grant_type: 'authorization_code',\n        code: 'code_000',\n        redirect_uri: 'http://example/redirect',\n        resource,\n      });\n    });\n  });\n\n  describe('validateResponse', () => {\n    const client = new OAuth2Client({\n      server: 'http://foo/',\n      tokenEndpoint: '/token',\n      clientId: 'test-client-id',\n    });\n\n    it('should correctly parse a valid URI from a OAUth2 server redirect', () => {\n      assert.deepEqual(\n        client.authorizationCode.validateResponse(\n          'https://example/?code=123&scope=scope1%20scope2',\n          {}\n        ),\n        {\n          code: '123',\n          scope: ['scope1', 'scope2'],\n        }\n      );\n    });\n    it('should work when paramaters are set into the fragment', () => {\n      assert.deepEqual(\n        client.authorizationCode.validateResponse(\n          'https://example/#code=123&scope=scope1%20scope2',\n          {}\n        ),\n        {\n          code: '123',\n          scope: ['scope1', 'scope2'],\n        }\n      );\n    });\n    it('should validate the state parameter', () => {\n      assert.deepEqual(\n        client.authorizationCode.validateResponse(\n          'https://example/?code=123&scope=scope1%20scope2&state=my-state',\n          { state: 'my-state' }\n        ),\n        {\n          code: '123',\n          scope: ['scope1', 'scope2'],\n        }\n      );\n    });\n    it('should error if the state did not match', () => {\n      let caught = false;\n      try {\n        client.authorizationCode.validateResponse(\n          'https://example/?code=123&scope=scope1%20scope2',\n          { state: 'my-state' }\n        );\n      } catch (_err) {\n        caught = true;\n      }\n      assert.equal(caught, true);\n    });\n  });\n});\n"
  },
  {
    "path": "test/client-credentials.ts",
    "content": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client, OAuth2HttpError } from '../src/index.ts';\nimport { after, describe, it } from 'node:test';\n\ndescribe('client-credentials', () => {\n  let server: ReturnType<typeof testServer>;\n\n  after(async () => {\n    if (server) {\n      await server.close();\n    }\n\n  });\n  it('should work with client_secret_basic', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId:10',\n      clientSecret: 'test=client=secret',\n      authenticationMethod: 'client_secret_basic',\n    });\n\n    const result = await client.clientCredentials();\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId%3A10:test%3Dclient%3Dsecret')\n    );\n\n    assert.deepEqual(request.body, {\n      grant_type: 'client_credentials',\n    });\n  });\n  it('should apply \"interop\" encoding when using client_secret_basic_interop', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId:10',\n      clientSecret: 'test=client=secret',\n      authenticationMethod: 'client_secret_basic_interop',\n    });\n\n    const result = await client.clientCredentials();\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId%3A10:test=client=secret')\n    );\n\n    assert.deepEqual(request.body, {\n      grant_type: 'client_credentials',\n    });\n  });\n  it('should apply \"interop\" encoding by default when no authenticationMethod is provided', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId:10',\n      clientSecret: 'test=client=secret',\n    });\n\n    const result = await client.clientCredentials();\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId%3A10:test=client=secret')\n    );\n\n    assert.deepEqual(request.body, {\n      grant_type: 'client_credentials',\n    });\n  });\n  it('should support extra parameters', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n    });\n\n    const result = await client.clientCredentials({\n      extraParams: {\n        foo: 'bar',\n      },\n    });\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId:testClientSecret')\n    );\n    assert.equal(request.headers.get('Accept'), 'application/json');\n\n    assert.deepEqual(request.body, {\n      grant_type: 'client_credentials',\n      foo: 'bar',\n    });\n  });\n\n  it('should work with client_secret_post', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n      authenticationMethod: 'client_secret_post',\n    });\n\n    const result = await client.clientCredentials();\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n\n    assert.deepEqual(request.body, {\n      client_id: 'testClientId',\n      client_secret: 'testClientSecret',\n      grant_type: 'client_credentials',\n    });\n  });\n  it('should support the resource parameter', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n    });\n\n    const resource = ['https://example/resource1', 'https://example/resource2'];\n\n    const result = await client.clientCredentials({\n      resource,\n    });\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId:testClientSecret')\n    );\n\n    assert.deepEqual(request.body, {\n      grant_type: 'client_credentials',\n      resource,\n    });\n  });\n  it('should return an idToken if it was returned from the server', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n    });\n    const token = await client.tokenResponseToOAuth2Token(\n      Promise.resolve({\n        token_type: 'bearer',\n        access_token: 'foo',\n        id_token: 'bar',\n        refresh_token: 'baz',\n      })\n    );\n\n    assert.deepEqual(token, {\n      accessToken: 'foo',\n      idToken: 'bar',\n      expiresAt: null,\n      refreshToken: 'baz',\n    });\n  });\n\n  describe('error handling', async () => {\n    it('should create a OAuth2HttpError if an error was thrown', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'oauth2-error',\n        clientSecret: 'testClientSecret',\n        authenticationMethod: 'client_secret_post',\n      });\n\n      const resource = [\n        'https://example/resource1',\n        'https://example/resource2',\n      ];\n\n      try {\n        await client.clientCredentials({\n          resource,\n        });\n        throw new Error('This operation should have failed');\n      } catch (err: any) {\n        assert.ok(err instanceof OAuth2HttpError);\n        assert.ok(err.response instanceof Response);\n        assert.equal(err.oauth2Code, 'invalid_client');\n        assert.deepEqual(err.parsedBody, {\n          error: 'invalid_client',\n          error_description: 'OOps!',\n        });\n      }\n    });\n    it('should create a OAuth2HttpError also if a non-oauth2 error was thrown with a JSON response', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'json-error',\n        clientSecret: 'testClientSecret',\n        authenticationMethod: 'client_secret_post',\n      });\n\n      const resource = [\n        'https://example/resource1',\n        'https://example/resource2',\n      ];\n\n      try {\n        await client.clientCredentials({\n          resource,\n        });\n        throw new Error('This operation should have failed');\n      } catch (err: any) {\n        assert.ok(err instanceof OAuth2HttpError);\n        assert.ok(err.response instanceof Response);\n        assert.equal(err.httpCode, 418);\n        assert.equal(err.oauth2Code, null);\n        assert.deepEqual(err.parsedBody, {\n          status: 418,\n          title: 'OOps!',\n          type: 'https://example/dummy',\n        });\n      }\n    });\n    it('should create a OAuth2HttpError when a generic HTTP error was thrown ', async () => {\n      server = testServer();\n\n      const client = new OAuth2Client({\n        server: server.url,\n        tokenEndpoint: '/token',\n        clientId: 'general-http-error',\n        clientSecret: 'testClientSecret',\n        authenticationMethod: 'client_secret_post',\n      });\n\n      const resource = [\n        'https://example/resource1',\n        'https://example/resource2',\n      ];\n\n      try {\n        await client.clientCredentials({\n          resource,\n        });\n        throw new Error('This operation should have failed');\n      } catch (err: any) {\n        assert.ok(err instanceof OAuth2HttpError);\n        assert.ok(err.response instanceof Response);\n        assert.equal(err.oauth2Code, null);\n        assert.equal(err.parsedBody, undefined);\n      }\n    });\n\n  });\n});\n"
  },
  {
    "path": "test/client.ts",
    "content": "import * as assert from 'node:assert';\nimport { OAuth2Client } from '../src/index.ts';\nimport { legacyFormUrlEncode } from '../src/client.ts';\nimport { describe, it } from 'node:test';\n\ndescribe('tokenResponseToOAuth2Token', () => {\n  it('should convert a JSON response to a OAuth2Token', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n    });\n    const token = await client.tokenResponseToOAuth2Token(\n      Promise.resolve({\n        token_type: 'bearer',\n        access_token: 'foo-bar',\n        scope: 'foo bar',\n        foo: 'bar'\n      })\n    );\n\n    assert.deepEqual(token, {\n      accessToken: 'foo-bar',\n      expiresAt: null,\n      refreshToken: null,\n      scope: ['foo', 'bar'],\n      extraParams: {\n        foo: 'bar'\n      }\n    });\n  });\n\n  it('should error when an invalid JSON object is passed', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n    });\n\n    let caught = false;\n    try {\n      await client.tokenResponseToOAuth2Token(\n        Promise.resolve({\n          funzies: 'foo-bar',\n        } as any)\n      );\n    } catch (err) {\n      assert.ok(err instanceof TypeError);\n      caught = true;\n    }\n\n    assert.equal(caught, true);\n  });\n  it('should error when an empty body is passed', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n    });\n\n    let caught = false;\n    try {\n      await client.tokenResponseToOAuth2Token(\n        Promise.resolve(undefined as any)\n      );\n    } catch (err) {\n      assert.ok(err instanceof TypeError);\n      caught = true;\n    }\n\n    assert.equal(caught, true);\n  });\n});\n\ndescribe('legacyFormUrlEncode', () => {\n  it('correctly encodes full character set', () => {\n    const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~ ó';\n    assert.equal(\n      legacyFormUrlEncode(chars),\n      '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');\n  });\n});\n\ndescribe('getEndpoint', () => {\n  it('should not have a race condition when getting endpoints multiple times before the discovery request comes back', async () => {\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    const response = {\n      ok: true,\n      headers: new Headers([['Content-Type', 'application/json']]),\n      json() {\n        return Promise.resolve({\n          'authorization_endpoint': 'custom'\n        });\n      },\n    } as Response;\n\n    const client = new OAuth2Client({\n      server: 'http://server',\n      discoveryEndpoint: '/discovery',\n      clientId: 'clientId',\n      fetch: () => Promise.resolve(response)\n    });\n\n    const endpoint1 = client.getEndpoint('authorizationEndpoint');\n    const endpoint2 = client.getEndpoint('authorizationEndpoint');\n\n    assert.equal(await endpoint1, 'http://server/custom');\n    assert.equal(await endpoint2, 'http://server/custom');\n  });\n});\n"
  },
  {
    "path": "test/fetch-wrapper.ts",
    "content": "import * as assert from 'node:assert';\nimport { OAuth2Fetch, OAuth2Client } from '../src/index.ts';\nimport { afterEach, describe, it } from 'node:test';\n\ndescribe('FetchWrapper', () => {\n  let fetchWrapper: any;\n\n  afterEach(() => {\n    if (fetchWrapper) {\n      clearTimeout(fetchWrapper.refreshTimer);\n    }\n  });\n\n  it('should use the token from getNewToken', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n      clientSecret: 'bar',\n    });\n\n    fetchWrapper = new OAuth2Fetch({\n      client,\n      getNewToken: () => {\n        return {\n          accessToken: 'access',\n          refreshToken: 'refresh',\n          expiresAt: Date.now() + 1000_0000,\n        };\n      },\n    });\n\n    const mw = fetchWrapper.mw();\n\n    const response = await mw(\n      new Request('http://example/'),\n      (req): any => req\n    );\n\n    assert.equal(response.headers.get('Authorization'), 'Bearer access');\n  });\n\n  it(\"should use the token even if it's delayed\", async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n      clientSecret: 'bar',\n    });\n\n    fetchWrapper = new OAuth2Fetch({\n      client,\n      getNewToken: async () => {\n        await new Promise((res) => setTimeout(res, 200));\n        return {\n          accessToken: 'access',\n          refreshToken: 'refresh',\n          expiresAt: Date.now() + 1000_0000,\n        };\n      },\n    });\n\n    const mw = fetchWrapper.mw();\n\n    const response = await mw(\n      new Request('http://example/'),\n      (req): any => req\n    );\n\n    assert.equal(response.headers.get('Authorization'), 'Bearer access');\n  });\n\n  it('should use a token from getStoredToken', async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n      clientSecret: 'bar',\n    });\n\n    fetchWrapper = new OAuth2Fetch({\n      client,\n      getNewToken: () => null,\n      getStoredToken: () => {\n        return {\n          accessToken: 'access',\n          refreshToken: 'refresh',\n          expiresAt: Date.now() + 1000_0000,\n        };\n      },\n    });\n\n    const mw = fetchWrapper.mw();\n\n    const response = await mw(\n      new Request('http://example/'),\n      (req): any => req\n    );\n\n    assert.equal(response.headers.get('Authorization'), 'Bearer access');\n  });\n\n  it(\"should still work with getStoredToken even if it's delayed\", async () => {\n    const client = new OAuth2Client({\n      clientId: 'foo',\n      clientSecret: 'bar',\n    });\n\n    fetchWrapper = new OAuth2Fetch({\n      client,\n      getNewToken: () => null,\n      getStoredToken: async () => {\n        await new Promise((res) => setTimeout(res, 200));\n        return {\n          accessToken: 'access',\n          refreshToken: 'refresh',\n          expiresAt: Date.now() + 1000_0000,\n        };\n      },\n    });\n\n    const mw = fetchWrapper.mw();\n\n    const response = await mw(\n      new Request('http://example/'),\n      (req): any => req\n    );\n\n    assert.equal(response.headers.get('Authorization'), 'Bearer access');\n  });\n});\n"
  },
  {
    "path": "test/password.ts",
    "content": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../src/index.ts';\nimport { after, describe, it } from 'node:test';\n\ndescribe('password', () => {\n  let server: ReturnType<typeof testServer>;\n\n  after(async () => {\n    if (server) {\n      await server.close();\n    }\n  });\n\n  it('should work with client_secret_basic', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n    });\n\n    const result = await client.password({\n      username: 'user123',\n      password: 'password',\n    });\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId:testClientSecret')\n    );\n    assert.equal(request.headers.get('Accept'), 'application/json');\n\n    assert.deepEqual(request.body, {\n      grant_type: 'password',\n      password: 'password',\n      username: 'user123',\n    });\n  });\n\n  it('should work with client_secret_post', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      authenticationMethod: 'client_secret_post',\n    });\n\n    const result = await client.password({\n      username: 'user123',\n      password: 'password',\n    });\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n\n    assert.deepEqual(request.body, {\n      grant_type: 'password',\n      password: 'password',\n      username: 'user123',\n      client_id: 'testClientId',\n    });\n  });\n\n  it('should support the resource parameter', async () => {\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      authenticationMethod: 'client_secret_post',\n    });\n    const resource = ['https://example/resource1', 'https://example/resource2'];\n\n    const result = await client.password({\n      username: 'user123',\n      password: 'password',\n      resource,\n    });\n\n    assert.equal(result.accessToken, 'access_token_000');\n    assert.equal(result.refreshToken, 'refresh_token_000');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n\n    assert.deepEqual(request.body, {\n      grant_type: 'password',\n      password: 'password',\n      username: 'user123',\n      client_id: 'testClientId',\n      resource,\n    });\n  });\n});\n"
  },
  {
    "path": "test/pkce.ts",
    "content": "import * as assert from 'node:assert';\nimport { generateCodeVerifier } from '../src/index.ts';\nimport { getCodeChallenge } from '../src/client/authorization-code.ts';\nimport { describe, it } from 'node:test';\n\ndescribe('generateCodeVerifier', () => {\n\n  it('should generate a 32byte base4url string', async () => {\n\n    const out = await generateCodeVerifier();\n    //console.debug(out, out.length);\n    assert.match(out,/^[A-Za-z0-9-_]{43}$/);\n\n  });\n\n});\n\ndescribe('getCodeChallenge', () => {\n\n  it('should generate the matching code challenge for a given code verifier', async() => {\n\n    const codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';\n    const codeChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';\n\n    assert.deepEqual(await getCodeChallenge(codeVerifier),['S256', codeChallenge]);\n\n  });\n\n});\n"
  },
  {
    "path": "test/refresh.ts",
    "content": "import { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../src/index.ts';\nimport * as assert from 'node:assert';\nimport { after, describe, it } from 'node:test';\n\ndescribe('refreshing tokens', () => {\n\n  let server: ReturnType<typeof testServer>;\n  after(async () => {\n    if (server) {\n      await server.close();\n    }\n\n  });\n  it('should work', async () => {\n\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n    });\n\n    const result = await client.refreshToken({\n      refreshToken: 'refresh_token_000',\n      accessToken: 'access_token_000',\n      expiresAt: null,\n    });\n\n    assert.equal(result.accessToken, 'access_token_001');\n    assert.equal(result.refreshToken, 'refresh_token_001');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId:testClientSecret')\n    );\n    assert.equal(\n      request.headers.get('Accept'),\n      'application/json'\n    );\n\n    assert.deepEqual(request.body,{\n      grant_type: 'refresh_token',\n      refresh_token: 'refresh_token_000',\n    });\n  });\n\n  it('should re-use the old refresh token if no new one was issued', async () => {\n\n    server = testServer();\n\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      clientId: 'testClientId',\n      clientSecret: 'testClientSecret',\n    });\n\n    const result = await client.refreshToken({\n      refreshToken: 'refresh_token_001',\n      accessToken: 'access_token_001',\n      expiresAt: null,\n    });\n\n    assert.equal(result.accessToken, 'access_token_002');\n    assert.equal(result.refreshToken, 'refresh_token_001');\n    assert.ok((result.expiresAt as number) <= Date.now() + 3600_000);\n    assert.ok((result.expiresAt as number) >= Date.now() + 3500_000);\n\n    const request = server.lastRequest();\n    assert.equal(\n      request.headers.get('Authorization'),\n      'Basic ' + btoa('testClientId:testClientSecret')\n    );\n    assert.equal(\n      request.headers.get('Accept'),\n      'application/json'\n    );\n\n    assert.deepEqual(request.body,{\n      grant_type: 'refresh_token',\n      refresh_token: 'refresh_token_001',\n    });\n  });\n});\n"
  },
  {
    "path": "test/revoke.ts",
    "content": "import * as assert from 'node:assert';\nimport { testServer } from './test-server.ts';\nimport { OAuth2Client } from '../src/index.ts';\nimport { after, describe, it } from 'node:test';\n\ndescribe('Token revocation', () => {\n  const server = testServer();\n\n  after(async () => {\n    if (server) {\n      await server.close();\n    }\n  });\n\n  describe('should revoke access token when requested', async () => {\n    const client = new OAuth2Client({\n      server: server.url,\n      tokenEndpoint: '/token',\n      revocationEndpoint: '/revoke',\n      clientId: 'test-client-id',\n      clientSecret: 'test-client-secret',\n    });\n\n    const token = await client.clientCredentials();\n\n    describe('When token type hint is not specified', () => {\n      it('should assume token type is access token', async () => {\n        await client.revoke(token);\n\n        const request = server.lastRequest();\n        assert.deepEqual(request.body, {\n          token: token.accessToken,\n          token_type_hint: 'access_token',\n        });\n      });\n    });\n\n    describe('When token type is specified as access token', () => {\n      it('should supply access token', async () => {\n        await client.revoke(token, 'access_token');\n\n        const request = server.lastRequest();\n        assert.deepEqual(request.body, {\n          token: token.accessToken,\n          token_type_hint: 'access_token',\n        });\n      });\n    });\n\n    describe('When token type is specified as refresh token', () => {\n      it('should supply access token', async () => {\n        await client.revoke(token, 'refresh_token');\n\n        const request = server.lastRequest();\n        assert.deepEqual(request.body, {\n          token: token.refreshToken,\n          token_type_hint: 'refresh_token',\n        });\n      });\n    });\n  });\n\n  describe('Discovery', () => {\n    const client = new OAuth2Client({\n      server: server.url,\n      discoveryEndpoint: '/discover',\n      clientId: 'test-client-id',\n    });\n\n    it('Should discover revocation endpoint', async () => {\n      const result = await client.getEndpoint('revocationEndpoint');\n      assert.deepEqual(result, server.url + '/revoke');\n    });\n  });\n});\n"
  },
  {
    "path": "test/test-server.ts",
    "content": "import { Application, type Middleware, Request } from '@curveball/core';\nimport bodyParser from '@curveball/bodyparser';\nimport * as http from 'http';\n\ntype TestServer = {\n  server: http.Server;\n  app: Application;\n  lastRequest: () => Request;\n  port: number;\n  url: string;\n  close: () => Promise<void>;\n}\n\nlet serverCache: null|TestServer = null;\n\nexport function testServer() {\n\n  if (serverCache) return serverCache;\n\n  let lastRequest: any = null;\n\n  const app = new Application();\n\n  app.use(bodyParser());\n  app.use((ctx, next) => {\n    lastRequest = ctx.request;\n    return next();\n  });\n  app.use(oauth2Error);\n  app.use(jsonError);\n  app.use(generalHttpError);\n  app.use(issueToken);\n  app.use(revokeToken);\n  app.use(discover);\n  const port = 40000 + Math.round(Math.random()*9999);\n  const server = app.listen(port);\n\n  serverCache = {\n    server,\n    app,\n    lastRequest: (): Request => lastRequest,\n    port,\n    url: 'http://localhost:' + port,\n    close: async() => {\n\n      return new Promise<void>(res => {\n        server.close(() => res());\n      });\n\n    }\n\n  };\n  return serverCache;\n\n}\n\nconst oauth2Error: Middleware = (ctx, next) => {\n\n  if (ctx.request.body?.client_id !== 'oauth2-error') {\n    return next();\n  }\n\n  ctx.response.body = {\n    error: 'invalid_client',\n    error_description: 'OOps!',\n  };\n\n  ctx.response.status = 400;\n  ctx.response.type = 'application/json';\n\n\n};\nconst jsonError: Middleware = (ctx, next) => {\n\n  if (ctx.request.body?.client_id !== 'json-error') {\n    return next();\n  }\n\n  ctx.response.body = {\n    type: 'https://example/dummy',\n    title: 'OOps!',\n    status: 418,\n  };\n\n  ctx.response.status = 418;\n  ctx.response.type = 'application/problem+json';\n\n};\nconst generalHttpError: Middleware = (ctx, next) => {\n\n  if (ctx.request.body?.client_id !== 'general-http-error') {\n    return next();\n  }\n\n  ctx.response.body = 'We\\'re super broken RN!';\n  ctx.response.status = 500;\n  ctx.response.type = 'text/plain';\n\n};\n\nconst issueToken: Middleware = (ctx, next) => {\n\n  if (ctx.path !== '/token') {\n    return next();\n  }\n\n  ctx.response.type = 'application/json';\n  if (ctx.request.body.refresh_token === 'refresh_token_000') {\n\n    ctx.response.body = {\n      token_type: 'Bearer',\n      access_token: 'access_token_001',\n      refresh_token: 'refresh_token_001',\n      expires_in: 3600,\n    };\n\n  } else if (ctx.request.body.refresh_token === 'refresh_token_001') {\n\n    ctx.response.body = {\n      token_type: 'Bearer',\n      access_token: 'access_token_002',\n      expires_in: 3600,\n    };\n\n  } else {\n\n    ctx.response.body = {\n      token_type: 'Bearer',\n      access_token: 'access_token_000',\n      refresh_token: 'refresh_token_000',\n      expires_in: 3600,\n      scope: 'foo bar',\n      foo: 'bar'  // Additional property returned by the server\n    };\n  }\n\n};\n\n\nconst revokeToken: Middleware = (ctx, next) => {\n\n  if (ctx.path !== '/revoke') {\n    return next();\n  }\n\n  ctx.response.type = 'application/octet-stream';\n  ctx.response.body = 'SUCCESS!';\n};\n\n\nconst discover: Middleware = (ctx, next) => {\n\n  if (ctx.path !== '/discover') {\n    return next();\n  }\n\n  ctx.response.type = 'application/json';\n  ctx.response.body = {\n    revocation_endpoint: '/revoke',\n  };\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"module\": \"esnext\",\n        \"target\": \"es2019\",\n\n        \"strict\": true,\n        \"noFallthroughCasesInSwitch\": true,\n        \"experimentalDecorators\": true,\n        \"noUnusedLocals\": true,\n        \"erasableSyntaxOnly\": true,\n\n        \"rewriteRelativeImportExtensions\": true,\n\n        \"moduleResolution\": \"node\",\n        \"sourceMap\": true,\n        \"outDir\": \"dist\",\n        \"baseUrl\": \".\",\n        \"paths\": {\n            \"*\": [\n                \"src/types/*\"\n            ]\n        },\n        \"lib\": [\n          \"DOM\",\n          \"ES2019\"\n        ],\n        \"declaration\": true\n    },\n    \"include\": [\n        \"src/**/*\"\n    ]\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    outDir: 'browser/',\n    lib: {\n      entry: 'src/index.ts',\n      fileName: (format) => `oauth2-client.min.js`,\n      formats: ['es'],\n    },\n    minify: true,\n  }\n});\n"
  }
]