diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..0d92402c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +.github +.nyc_output +coverage +docs/* +node_modules diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000..9e840f41 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,64 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + push: + branches: [master] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm + # with: + # version: 9 # Not needed if you've set "packageManager" in package.json + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c823f75..fa901fd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,18 +7,18 @@ in order to get your pull requests accepted. ## Disclose security vulnerabilities First things first: -This project has strong security implications and we appreciate every help to +This project has strong security implications, and we appreciate every help to improve security. -**However, please read our [security policy](./SECURITY.md), before taking -actions.** +**However, please read our [security policy](https://github.com/node-oauth/node-oauth2-server/security/policy), +before taking actions.** ## Guiding principles Before contributing to this project it is important to understand how this -project and it's collaborators views itself regarding it's scope and purpose. +project and it's collaborators views itself regarding its scope and purpose. ### OAuth2 standard compliance @@ -38,7 +38,7 @@ Extended readings: - [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) - [RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591) -### Framework agnostic +### Framework-agnostic Design decisions and implementations are always done with keeping in mind, that there are multiple frameworks out there that use this project. @@ -47,8 +47,7 @@ there are multiple frameworks out there that use this project. ## Development -If you want to fix bugs or add new features, **please read this chapter and it's -sections carefully!** +If you want to fix bugs or add new features, **please read this chapter and it's sections carefully!** ### No PR without issue @@ -60,24 +59,24 @@ and discuss, whether this is a useful addition to the project. First, clone and install this project from source via ```bash -$ git clone git@github.com:node-oauth/node-oauth2-server.git -$ cd node-oauth2-server -$ git checkout development # important! do not work on master! -$ npm install +git clone git@github.com:node-oauth/node-oauth2-server.git +cd node-oauth2-server +git checkout development # important! do not work on master! +npm install ``` From here you can run several scripts for development purposes: ```bash -$ npm run test # runs the tests once -$ npm run test:coverage # runs the tests including coverage -$ npm run docs # generates the API docs +npm run test # runs the tests once +npm run test:coverage # runs the tests including coverage +npm run docs # generates the API docs ``` To work on a new feature or a fix please create a new branch: ```bash -$ git checkout -b feature-xyz # or fix-xyz +git checkout -b feature-xyz # or fix-xyz ``` ### Coding rules @@ -93,8 +92,9 @@ with ticket number at the end of summary: ``` (): # ``` + Summary in present tense. Not capitalized. No period at the end. -The and fields are mandatory, the () and # field is optional. +The `` and `` fields are mandatory, the `()` and `#` fields are optional. ### Run the tests before committing @@ -105,13 +105,13 @@ the history with commits, that are solely targeting lint fixes. You can run the tests via ```bash -$ npm run test +npm run test ``` or ```bash -$ npm run test:coverage +npm run test:coverage ``` to see your coverage. @@ -152,7 +152,7 @@ Also make sure, to comply with the following list: #### Review process -Finally your PR needs to pass the review process: +Finally, your PR needs to pass the review process: - A certain amount of maintainers needs to review and accept your PR - Please **expect change requests**! They will occur and are intended to improve @@ -165,3 +165,31 @@ Finally your PR needs to pass the review process: #### After merge Please delete your branch after merge. + +## Documentation + +We use Vitepress+Markdown for our documentation. +If you want to contribute to the docs, please get familiar with Vitepress: https://vitepress.dev + +### Setting up docs + +You need NPM to set up the docs using the following: + +```shell +npm install +npm run docs:setup +``` + +You can then edit the `guide` section manually. + +### API Docs + +**DO NOT** edit the `api` section, as API docs are automatically +generated from our internal JSDoc comments. + +Instead, update the comments directly within the code and run `npm run docs:api` +to generate the api docs. + +### Building the docs + +Run `npm run docs:build` to build the docs. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..b7330346 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +.vitepress/cache +.vitepress/dist diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..9f85fc3c --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,110 @@ +import {defineConfig} from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "@node-oauth/oauth2-server", + description: "OAuth2 server for Node.js", + head: [['link', { rel: 'icon', href: '/images/favicon.ico' }]], + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + {text: 'Home', link: '/'}, + {text: 'Guide', link: '/guide/getting-started'}, + {text: 'API', link: '/api/server'} + ], + + sidebar: [ + { + text: 'Guide', + items: [ + {text: 'Getting started', link: '/guide/getting-started'}, + {text: 'Grant types', link: '/guide/grant-types'}, + {text: 'Model', link: '/guide/model'}, + {text: 'Token types', link: '/guide/token-types'}, + {text: 'PKCE', link: '/guide/pkce'}, + {text: 'Adapters', link: '/guide/adapters'}, + {text: 'Migrating to v5', link: '/guide/migrating-to-v5'}, + {text: 'Contributing', link: '/guide/contributing'}, + ] + }, + { + text: 'API', + items: [ + {text: 'OAuth2Server', link: '/api/server'}, + {text: 'Model', link: '/api/model'}, + {text: 'Request', link: '/api/request'}, + {text: 'Response', link: '/api/response'}, + { + text: 'Errors', items: [ + {text: 'Access Denied', link: '/api/errors/access-denied-error'}, + {text: 'Insufficient Scope', link: '/api/errors/insufficient-scope-error'}, + {text: 'Invalid Argument', link: '/api/errors/invalid-argument-error'}, + {text: 'Invalid Client', link: '/api/errors/invalid-client-error'}, + {text: 'Invalid Grant', link: '/api/errors/invalid-grant-error'}, + {text: 'Invalid Request', link: '/api/errors/invalid-request-error'}, + {text: 'Invalid Scope', link: '/api/errors/invalid-scope-error'}, + {text: 'Invalid Token', link: '/api/errors/invalid-token-error'}, + {text: 'OAuth Error', link: '/api/errors/oauth-error'}, + {text: 'Server Error', link: '/api/errors/server-error'}, + {text: 'Unauthorized Client', link: '/api/errors/unauthorized-client-error'}, + {text: 'Unauthorized Request', link: '/api/errors/unauthorized-request-error'}, + {text: 'Unsupported Grant Type', link: '/api/errors/unsupported-grant-type-error'}, + {text: 'Unsupported Response Type', link: '/api/errors/unsupported-response-type-error'}, + ] + }, + { + text: 'Grant Types', items: [ + { text: 'Abstract Grant Type', link: '/api/grant-types/abstract-grant-type' }, + { text: 'Authorization Code', link: '/api/grant-types/authorization-code-grant-type' }, + { text: 'Client Credentials', link: '/api/grant-types/client-credentials-grant-type' }, + { text: 'Password', link: '/api/grant-types/password-grant-type' }, + { text: 'Refresh Token', link: '/api/grant-types/refresh-token-grant-type' }, + ] + }, + { + text: 'Handlers', items: [ + { text: 'Authenticate Handler', link: '/api/handlers/authenticate-handler' }, + { text: 'Authorize Handler', link: '/api/handlers/authorize-handler' }, + { text: 'Token Handler', link: '/api/handlers/token-handler' }, + ] + }, + { + text: 'Models', items: [ + { text: 'Token Model', link: '/api/models/token-model' }, + ] + }, + { + text: 'PKCE', items: [ + { text: 'PKCE', link: '/api/pkce/pkce' }, + ] + }, + { + text: 'Response Types', items: [ + { text: 'Code', link: '/api/response-types/code-response-type' }, + { text: 'Token', link: '/api/response-types/token-response-type' }, + ] + }, + { + text: 'Token Types', items: [ + { text: 'Bearer', link: '/api/token-types/bearer-token-type' }, + { text: 'Mac', link: '/api/token-types/mac-token-type' }, + ] + }, + { + text: 'Utils', items: [ + { text: 'Crypto', link: '/api/utils/crypto-util' }, + { text: 'Date', link: '/api/utils/date-util' }, + { text: 'Scope', link: '/api/utils/scope-util' }, + { text: 'String', link: '/api/utils/string-util' }, + { text: 'Token', link: '/api/utils/token-util' }, + ] + }, + ] + } + ], + + socialLinks: [ + {icon: 'github', link: 'https://github.com/node-oauth/node-oauth2-server'} + ] + } +}) diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index f8cc19db..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/oauth2-server.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/oauth2-server.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/oauth2-server" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/oauth2-server" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/api/errors/access-denied-error.md b/docs/api/errors/access-denied-error.md new file mode 100644 index 00000000..68de8587 --- /dev/null +++ b/docs/api/errors/access-denied-error.md @@ -0,0 +1,16 @@ + + +## AccessDeniedError +"The resource owner or authorization server denied the request" + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + + +### new AccessDeniedError(message, [properties]) + +| Param | Type | +| --- | --- | +| message | string | +| [properties] | object | + diff --git a/docs/api/errors/access-denied-error.rst b/docs/api/errors/access-denied-error.rst deleted file mode 100644 index 11e61ec9..00000000 --- a/docs/api/errors/access-denied-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -=================== - AccessDeniedError -=================== - -The resource owner or authorization server denied the request. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const AccessDeniedError = require('@node-oauth/oauth2-server/lib/errors/access-denied-error'); - --------- - -.. _AccessDeniedError#constructor: - -``new AccessDeniedError(message, properties)`` -============================================== - -Instantiates an ``AccessDeniedError``. - -**Arguments:** - -+-----------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+===================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='access_denied'] | String | The error name used in responses generated from this error. | -+-----------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``AccessDeniedError``. - -**Remarks:** - -:: - - const err = new AccessDeniedError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'access_denied' - --------- - -.. _AccessDeniedError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _AccessDeniedError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _AccessDeniedError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _AccessDeniedError#name: - -``name`` -======== - -Typically ``'access_denied'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/index.rst b/docs/api/errors/index.rst deleted file mode 100644 index 382cbfa0..00000000 --- a/docs/api/errors/index.rst +++ /dev/null @@ -1,36 +0,0 @@ -======== - Errors -======== - -Noteable error types: - -``OAuthError`` - :doc:`oauth-error` is the base class for all exceptions thrown/returned by *oauth2-server*. - -``ServerError`` - :doc:`server-error` is used to wrap unknown exceptions encountered during request processing. - -``InvalidArgumentError`` - :doc:`invalid-argument-error` is thrown when an invalid argument is encountered. This error indicates that the module is used incorrectly and should never be seen because of external errors. - - -.. toctree:: - :maxdepth: 1 - :caption: Errors - :hidden: - - oauth-error - server-error - invalid-argument-error - access-denied-error - insufficient-scope-error - invalid-client-error - invalid-grant-error - invalid-request-error - invalid-scope-error - invalid-token-error - unauthorized-client-error - unauthorized-request-error - unsupported-grant-type-error - unsupported-response-type-error - diff --git a/docs/api/errors/insufficient-scope-error.md b/docs/api/errors/insufficient-scope-error.md new file mode 100644 index 00000000..c18e126f --- /dev/null +++ b/docs/api/errors/insufficient-scope-error.md @@ -0,0 +1,16 @@ + + +## InsufficientScopeError +"The request requires higher privileges than provided by the access token.." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6750.html#section-3.1 + + +### new InsufficientScopeError(message, [properties]) + +| Param | Type | +| --- | --- | +| message | string | +| [properties] | object | + diff --git a/docs/api/errors/insufficient-scope-error.rst b/docs/api/errors/insufficient-scope-error.rst deleted file mode 100644 index b19e2acb..00000000 --- a/docs/api/errors/insufficient-scope-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -======================== - InsufficientScopeError -======================== - -The request requires higher privileges than provided by the access token. See :rfc:`Section 3.1 of RFC 6750 <6750#section-3.1>`. - -:: - - const InsufficientScopeError = require('@node-oauth/oauth2-server/lib/errors/insufficient-scope-error'); - --------- - -.. _InsufficientScopeError#constructor: - -``new InsufficientScopeError(message, properties)`` -=================================================== - -Instantiates an ``InsufficientScopeError``. - -**Arguments:** - -+----------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+========================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=403] | Object | See :ref:`OAuthError#constructor`. | -+----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='insufficient_scope'] | String | The error name used in responses generated from this error. | -+----------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InsufficientScopeError``. - -**Remarks:** - -:: - - const err = new InsufficientScopeError(); - // err.message === 'Forbidden' - // err.code === 403 - // err.name === 'insufficient_scope' - --------- - -.. _InsufficientScopeError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InsufficientScopeError#code: - -``code`` -======== - -Typically ``403``. See :ref:`OAuthError#code `. - --------- - -.. _InsufficientScopeError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InsufficientScopeError#name: - -``name`` -======== - -Typically ``'insufficient_scope'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-argument-error.md b/docs/api/errors/invalid-argument-error.md new file mode 100644 index 00000000..5d080939 --- /dev/null +++ b/docs/api/errors/invalid-argument-error.md @@ -0,0 +1,15 @@ + + +## InvalidArgumentError +"An argument to a function or constructor is missing or of wrong type" + +**Kind**: global class + + +### new InvalidArgumentError(message, [properties]) + +| Param | Type | +| --- | --- | +| message | string | +| [properties] | object | + diff --git a/docs/api/errors/invalid-argument-error.rst b/docs/api/errors/invalid-argument-error.rst deleted file mode 100644 index 11b554eb..00000000 --- a/docs/api/errors/invalid-argument-error.rst +++ /dev/null @@ -1,84 +0,0 @@ -====================== - InvalidArgumentError -====================== - -An invalid argument was encountered. - -:: - - const InvalidArgumentError = require('@node-oauth/oauth2-server/lib/errors/invalid-argument-error'); - -.. note:: This error indicates that the module is used incorrectly (i.e., there is a programming error) and should never be seen because of external errors (like invalid data sent by a client). - --------- - -.. _InvalidArgumentError#constructor: - -``new InvalidArgumentError(message, properties)`` -================================================= - -Instantiates an ``InvalidArgumentError``. - -**Arguments:** - -+--------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+======================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+--------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+--------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=500] | Object | See :ref:`OAuthError#constructor`. | -+--------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_argument'] | String | The error name used in responses generated from this error. | -+--------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidArgumentError``. - -**Remarks:** - -:: - - const err = new InvalidArgumentError(); - // err.message === 'Internal Server Error' - // err.code === 500 - // err.name === 'invalid_argument' - --------- - -.. _InvalidArgumentError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidArgumentError#code: - -``code`` -======== - -Typically ``500``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidArgumentError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidArgumentError#name: - -``name`` -======== - -Typically ``'invalid_argument'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-client-error.md b/docs/api/errors/invalid-client-error.md new file mode 100644 index 00000000..552646cc --- /dev/null +++ b/docs/api/errors/invalid-client-error.md @@ -0,0 +1,17 @@ + + +## InvalidClientError +"Client authentication failed (e.g., unknown client, no client + authentication included, or unsupported authentication method)" + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-5.2 + + +### new InvalidClientError(message, [properties]) + +| Param | Type | +| --- | --- | +| message | string | +| [properties] | object | + diff --git a/docs/api/errors/invalid-client-error.rst b/docs/api/errors/invalid-client-error.rst deleted file mode 100644 index 5ddd0a40..00000000 --- a/docs/api/errors/invalid-client-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -==================== - InvalidClientError -==================== - -Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). See :rfc:`Section 5.2 of RFC 6749 <6749#section-5.2>`. - -:: - - const InvalidClientError = require('@node-oauth/oauth2-server/lib/errors/invalid-client-error'); - --------- - -.. _InvalidClientError#constructor: - -``new InvalidClientError(message, properties)`` -=============================================== - -Instantiates an ``InvalidClientError``. - -**Arguments:** - -+------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+====================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_client'] | String | The error name used in responses generated from this error. | -+------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidClientError``. - -**Remarks:** - -:: - - const err = new InvalidClientError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'invalid_client' - --------- - -.. _InvalidClientError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidClientError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidClientError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidClientError#name: - -``name`` -======== - -Typically ``'invalid_client'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-grant-error.md b/docs/api/errors/invalid-grant-error.md new file mode 100644 index 00000000..874e285d --- /dev/null +++ b/docs/api/errors/invalid-grant-error.md @@ -0,0 +1,18 @@ + + +## InvalidGrantError +"The provided authorization grant (e.g., authorization code, resource owner credentials) +or refresh token is invalid, expired, revoked, does not match the redirection URI used +in the authorization request, or was issued to another client." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-5.2 + + +### new InvalidGrantError(message, [properties]) + +| Param | Type | +| --- | --- | +| message | string | +| [properties] | object | + diff --git a/docs/api/errors/invalid-grant-error.rst b/docs/api/errors/invalid-grant-error.rst deleted file mode 100644 index 79317149..00000000 --- a/docs/api/errors/invalid-grant-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -=================== - InvalidGrantError -=================== - -The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. See :rfc:`Section 5.2 of RFC 6749 <6749#section-5.2>`. - -:: - - const InvalidGrantError = require('@node-oauth/oauth2-server/lib/errors/invalid-grant-error'); - --------- - -.. _InvalidGrantError#constructor: - -``new InvalidGrantError(message, properties)`` -============================================== - -Instantiates an ``InvalidGrantError``. - -**Arguments:** - -+-----------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+===================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_grant'] | String | The error name used in responses generated from this error. | -+-----------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidGrantError``. - -**Remarks:** - -:: - - const err = new InvalidGrantError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'invalid_grant' - --------- - -.. _InvalidGrantError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidGrantError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidGrantError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidGrantError#name: - -``name`` -======== - -Typically ``'invalid_grant'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-request-error.md b/docs/api/errors/invalid-request-error.md new file mode 100644 index 00000000..5a40acd0 --- /dev/null +++ b/docs/api/errors/invalid-request-error.md @@ -0,0 +1,10 @@ + + +## InvalidRequest +Constructor. + +"The request is missing a required parameter, includes an invalid parameter value, +includes a parameter more than once, or is otherwise malformed." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.2.2.1 diff --git a/docs/api/errors/invalid-request-error.rst b/docs/api/errors/invalid-request-error.rst deleted file mode 100644 index bbb38c44..00000000 --- a/docs/api/errors/invalid-request-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -===================== - InvalidRequestError -===================== - -The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. See :rfc:`Section 4.2.2.1 of RFC 6749 <6749#section-4.2.2.1>`. - -:: - - const InvalidRequestError = require('@node-oauth/oauth2-server/lib/errors/invalid-request-error'); - --------- - -.. _InvalidRequestError#constructor: - -``new InvalidRequestError(message, properties)`` -================================================ - -Instantiates an ``InvalidRequestError``. - -**Arguments:** - -+-------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+=====================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_request'] | String | The error name used in responses generated from this error. | -+-------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidRequestError``. - -**Remarks:** - -:: - - const err = new InvalidRequestError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'invalid_request' - --------- - -.. _InvalidRequestError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidRequestError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidRequestError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidRequestError#name: - -``name`` -======== - -Typically ``'invalid_request'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-scope-error.md b/docs/api/errors/invalid-scope-error.md new file mode 100644 index 00000000..d071ffe9 --- /dev/null +++ b/docs/api/errors/invalid-scope-error.md @@ -0,0 +1,9 @@ + + +## InvalidScopeError +Constructor. + +"The requested scope is invalid, unknown, or malformed." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 diff --git a/docs/api/errors/invalid-scope-error.rst b/docs/api/errors/invalid-scope-error.rst deleted file mode 100644 index 01c70d26..00000000 --- a/docs/api/errors/invalid-scope-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -=================== - InvalidScopeError -=================== - -The requested scope is invalid, unknown, or malformed. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const InvalidScopeError = require('@node-oauth/oauth2-server/lib/errors/invalid-scope-error'); - --------- - -.. _InvalidScopeError#constructor: - -``new InvalidScopeError(message, properties)`` -============================================== - -Instantiates an ``InvalidScopeError``. - -**Arguments:** - -+-----------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+===================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_scope'] | String | The error name used in responses generated from this error. | -+-----------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidScopeError``. - -**Remarks:** - -:: - - const err = new InvalidScopeError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'invalid_scope' - --------- - -.. _InvalidScopeError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidScopeError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidScopeError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidScopeError#name: - -``name`` -======== - -Typically ``'invalid_scope'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/invalid-token-error.md b/docs/api/errors/invalid-token-error.md new file mode 100644 index 00000000..f9ecd467 --- /dev/null +++ b/docs/api/errors/invalid-token-error.md @@ -0,0 +1,9 @@ + + +## InvalidTokenError +Constructor. + +"The access token provided is expired, revoked, malformed, or invalid for other reasons." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6750#section-3.1 diff --git a/docs/api/errors/invalid-token-error.rst b/docs/api/errors/invalid-token-error.rst deleted file mode 100644 index fc0da035..00000000 --- a/docs/api/errors/invalid-token-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -=================== - InvalidTokenError -=================== - -The access token provided is expired, revoked, malformed, or invalid for other reasons. See :rfc:`Section 3.1 of RFC 6750 <6750#section-3.1>`. - -:: - - const InvalidTokenError = require('@node-oauth/oauth2-server/lib/errors/invalid-token-error'); - --------- - -.. _InvalidTokenError#constructor: - -``new InvalidTokenError(message, properties)`` -============================================== - -Instantiates an ``InvalidTokenError``. - -**Arguments:** - -+-----------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+===================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=401] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='invalid_token'] | String | The error name used in responses generated from this error. | -+-----------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``InvalidTokenError``. - -**Remarks:** - -:: - - const err = new InvalidTokenError(); - // err.message === 'Unauthorized' - // err.code === 401 - // err.name === 'invalid_token' - --------- - -.. _InvalidTokenError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _InvalidTokenError#code: - -``code`` -======== - -Typically ``401``. See :ref:`OAuthError#code `. - --------- - -.. _InvalidTokenError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _InvalidTokenError#name: - -``name`` -======== - -Typically ``'invalid_token'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/oauth-error.md b/docs/api/errors/oauth-error.md new file mode 100644 index 00000000..1d1c947e --- /dev/null +++ b/docs/api/errors/oauth-error.md @@ -0,0 +1,15 @@ + + +## OAuthError +Baseclass for OAuth related Error types. + +**Kind**: global class + + +### new OAuthError(messageOrError, properties) + +| Param | +| --- | +| messageOrError | +| properties | + diff --git a/docs/api/errors/oauth-error.rst b/docs/api/errors/oauth-error.rst deleted file mode 100644 index 83be4659..00000000 --- a/docs/api/errors/oauth-error.rst +++ /dev/null @@ -1,124 +0,0 @@ -============ - OAuthError -============ - -Base class for all errors returned by this module. - -:: - - const OAuthError = require('@node-oauth/oauth2-server/lib/errors/oauth-error'); - --------- - -.. _OAuthError#constructor: - -``new OAuthError(message, properties)`` -======================================= - -Instantiates ``OAuthError``. - -.. note:: Do not use ``OAuthError`` directly; it's intended to be used as a base class. Instead, use one of the other error types derived from it. - -**Arguments:** - -+-----------------------------+--------------+------------------------------------------------------------------------------------+ -| Name | Type | Description | -+=============================+==============+====================================================================================+ -| [message=undefined] | String|Error | Error message or nested exception. | -+-----------------------------+--------------+------------------------------------------------------------------------------------+ -| [properties={}] | Object | Additional properties to be set on the error object. | -+-----------------------------+--------------+------------------------------------------------------------------------------------+ -| [properties.code=500] | Object | An HTTP status code associated with the error. | -+-----------------------------+--------------+------------------------------------------------------------------------------------+ -| [properties.name=undefined] | String | The name of the error. If left undefined, the name is copied from the constructor. | -+-----------------------------+--------------+------------------------------------------------------------------------------------+ - -**Return value:** - -A new instance of ``OAuthError``. - -**Remarks:** - -By default ``code`` is set to ``500`` and ``message`` is set to the respective HTTP phrase. - -:: - - const err = new OAuthError(); - // err.message === 'Internal Server Error' - // err.code === 500 - // err.name === 'OAuthError' - -:: - - const err = new OAuthError('test', {name: 'test_error'}); - // err.message === 'test' - // err.code === 500 - // err.name === 'test_error' - -:: - - const err = new OAuthError(undefined, {code: 404}); - // err.message === 'Not Found' - // err.code === 404 - // err.name === 'OAuthError' - -All additional ``properties`` are copied to the error object. - -:: - - const err = new OAuthError('test', {foo: 'bar', baz: 1234}); - // err.message === 'test' - // err.code === 500 - // err.name === 'OAuthError' - // err.foo === 'bar' - // err.baz === 1234 - -When wrapping an exception, the ``message`` property is automatically copied from the existing exception. - -:: - - const anotherError = new Error('test'); - const err = new OAuthError(e); - // err.message === 'test' - // err.code === 500 - // err.name === 'OAuthError' - // err.inner === anotherError - --------- - -.. _OAuthError#message: - -``message`` -=========== - -A message describing the error. - --------- - -.. _OAuthError#code: - -``code`` -======== - -An HTTP status code associated with the error. - -For compatibility reasons, two more properties exist that have the same value as ``code``: ``status`` and ``statusCode``. Note that changes to one of these are not reflected by the other properties. - --------- - -.. _OAuthError#inner: - -``inner`` -========= - -Another exception that was wrapped by this ``OAuthError`` instance. This property is set only if the error is constructed from an existing exception. - --------- - -.. _OAuthError#name: - -``name`` -======== - -The name of the error, intended to be used as the ``error`` parameter as described by :rfc:`6749` in :rfc:`Section 4.1.2.1 <6749#section-4.1.2.1>`, :rfc:`Section 4.2.2.1 <6749#section-4.2.2.1>` and :rfc:`Section 5.2 <6749#section-5.2>`, as well as :rfc:`Section 3.1 of RFC 6750 <6750#section-3.1>`. - diff --git a/docs/api/errors/server-error.md b/docs/api/errors/server-error.md new file mode 100644 index 00000000..e0796fce --- /dev/null +++ b/docs/api/errors/server-error.md @@ -0,0 +1,16 @@ + + +## ServerError +"The authorization server encountered an unexpected condition that prevented it from fulfilling the request." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + + +### new ServerError(message, properties) + +| Param | +| --- | +| message | +| properties | + diff --git a/docs/api/errors/server-error.rst b/docs/api/errors/server-error.rst deleted file mode 100644 index 7a2dcf90..00000000 --- a/docs/api/errors/server-error.rst +++ /dev/null @@ -1,84 +0,0 @@ -============= - ServerError -============= - -The authorization server encountered an unexpected condition that prevented it from fulfilling the request. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const ServerError = require('@node-oauth/oauth2-server/lib/errors/server-error'); - -``ServerError`` is used to wrap unknown exceptions encountered during request processing. - --------- - -.. _ServerError#constructor: - -``new ServerError(message, properties)`` -======================================== - -Instantiates an ``ServerError``. - -**Arguments:** - -+----------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+==================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+----------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=503] | Object | See :ref:`OAuthError#constructor`. | -+----------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='server_error'] | String | The error name used in responses generated from this error. | -+----------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``ServerError``. - -**Remarks:** - -:: - - const err = new ServerError(); - // err.message === 'Service Unavailable Error' - // err.code === 503 - // err.name === 'server_error' - --------- - -.. _ServerError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _ServerError#code: - -``code`` -======== - -Typically ``503``. See :ref:`OAuthError#code `. - --------- - -.. _ServerError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _ServerError#name: - -``name`` -======== - -Typically ``'server_error'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/unauthorized-client-error.md b/docs/api/errors/unauthorized-client-error.md new file mode 100644 index 00000000..5d8526e3 --- /dev/null +++ b/docs/api/errors/unauthorized-client-error.md @@ -0,0 +1,9 @@ + + +## UnauthorizedClientError +Constructor. + +"The authenticated client is not authorized to use this authorization grant type." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 diff --git a/docs/api/errors/unauthorized-client-error.rst b/docs/api/errors/unauthorized-client-error.rst deleted file mode 100644 index 9d104cac..00000000 --- a/docs/api/errors/unauthorized-client-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -========================= - UnauthorizedClientError -========================= - -The authenticated client is not authorized to use this authorization grant type. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const UnauthorizedClientError = require('@node-oauth/oauth2-server/lib/errors/unauthorized-client-error'); - --------- - -.. _UnauthorizedClientError#constructor: - -``new UnauthorizedClientError(message, properties)`` -==================================================== - -Instantiates an ``UnauthorizedClientError``. - -**Arguments:** - -+-----------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+=========================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='unauthorized_client'] | String | The error name used in responses generated from this error. | -+-----------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``UnauthorizedClientError``. - -**Remarks:** - -:: - - const err = new UnauthorizedClientError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'unauthorized_client' - --------- - -.. _UnauthorizedClientError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _UnauthorizedClientError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _UnauthorizedClientError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _UnauthorizedClientError#name: - -``name`` -======== - -Typically ``'unauthorized_client'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/unauthorized-request-error.md b/docs/api/errors/unauthorized-request-error.md new file mode 100644 index 00000000..2764fa42 --- /dev/null +++ b/docs/api/errors/unauthorized-request-error.md @@ -0,0 +1,12 @@ + + +## UnauthorizedRequestError +Constructor. + +"If the request lacks any authentication information (e.g., the client +was unaware that authentication is necessary or attempted using an +unsupported authentication method), the resource server SHOULD NOT +include an error code or other error information." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6750#section-3.1 diff --git a/docs/api/errors/unauthorized-request-error.rst b/docs/api/errors/unauthorized-request-error.rst deleted file mode 100644 index 9ed24675..00000000 --- a/docs/api/errors/unauthorized-request-error.rst +++ /dev/null @@ -1,89 +0,0 @@ -========================== - UnauthorizedRequestError -========================== - -The request lacked any authentication information or the client attempted to use an unsupported authentication method. - -:: - - const UnauthorizedRequestError = require('@node-oauth/oauth2-server/lib/errors/unauthorized-request-error'); - -According to :rfc:`Section 3.1 of RFC 6750 <6750#section-3.1>` you should just fail the request with ``401 Unauthorized`` and not send any error information in the body if this error occurs: - - If the request lacks any authentication information (e.g., the client - was unaware that authentication is necessary or attempted using an - unsupported authentication method), the resource server SHOULD NOT - include an error code or other error information. - --------- - -.. _UnauthorizedRequestError#constructor: - -``new UnauthorizedRequestError(message, properties)`` -===================================================== - -Instantiates an ``UnauthorizedRequestError``. - -**Arguments:** - -+------------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+==========================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=401] | Object | See :ref:`OAuthError#constructor`. | -+------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='unauthorized_request'] | String | The error name used in responses generated from this error. | -+------------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``UnauthorizedRequestError``. - -**Remarks:** - -:: - - const err = new UnauthorizedRequestError(); - // err.message === 'Unauthorized' - // err.code === 401 - // err.name === 'unauthorized_request' - --------- - -.. _UnauthorizedRequestError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _UnauthorizedRequestError#code: - -``code`` -======== - -Typically ``401``. See :ref:`OAuthError#code `. - --------- - -.. _UnauthorizedRequestError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _UnauthorizedRequestError#name: - -``name`` -======== - -Typically ``'unauthorized_request'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/unsupported-grant-type-error.md b/docs/api/errors/unsupported-grant-type-error.md new file mode 100644 index 00000000..4ac94ae5 --- /dev/null +++ b/docs/api/errors/unsupported-grant-type-error.md @@ -0,0 +1,9 @@ + + +## UnsupportedGrantTypeError +Constructor. + +"The authorization grant type is not supported by the authorization server." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 diff --git a/docs/api/errors/unsupported-grant-type-error.rst b/docs/api/errors/unsupported-grant-type-error.rst deleted file mode 100644 index 1e812ed7..00000000 --- a/docs/api/errors/unsupported-grant-type-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -=========================== - UnsupportedGrantTypeError -=========================== - -The authorization grant type is not supported by the authorization server. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const UnsupportedGrantTypeError = require('@node-oauth/oauth2-server/lib/errors/unsupported-grant-type-error'); - --------- - -.. _UnsupportedGrantTypeError#constructor: - -``new UnsupportedGrantTypeError(message, properties)`` -====================================================== - -Instantiates an ``UnsupportedGrantTypeError``. - -**Arguments:** - -+--------------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+============================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+--------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+--------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+--------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='unsupported_grant_type'] | String | The error name used in responses generated from this error. | -+--------------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``UnsupportedGrantTypeError``. - -**Remarks:** - -:: - - const err = new UnsupportedGrantTypeError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'unsupported_grant_type' - --------- - -.. _UnsupportedGrantTypeError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _UnsupportedGrantTypeError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _UnsupportedGrantTypeError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _UnsupportedGrantTypeError#name: - -``name`` -======== - -Typically ``'unsupported_grant_type'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/errors/unsupported-response-type-error.md b/docs/api/errors/unsupported-response-type-error.md new file mode 100644 index 00000000..a245af47 --- /dev/null +++ b/docs/api/errors/unsupported-response-type-error.md @@ -0,0 +1,10 @@ + + +## UnsupportedResponseTypeError +Constructor. + +"The authorization server does not supported obtaining an +authorization code using this method." + +**Kind**: global class +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 diff --git a/docs/api/errors/unsupported-response-type-error.rst b/docs/api/errors/unsupported-response-type-error.rst deleted file mode 100644 index c9ee0fd3..00000000 --- a/docs/api/errors/unsupported-response-type-error.rst +++ /dev/null @@ -1,82 +0,0 @@ -============================== - UnsupportedResponseTypeError -============================== - -The authorization server does not supported obtaining an authorization code using this method. See :rfc:`Section 4.1.2.1 of RFC 6749 <6749#section-4.1.2.1>`. - -:: - - const UnsupportedResponseTypeError = require('@node-oauth/oauth2-server/lib/errors/unsupported-response-type-error'); - --------- - -.. _UnsupportedResponseTypeError#constructor: - -``new UnsupportedResponseTypeError(message, properties)`` -========================================================= - -Instantiates an ``UnsupportedResponseTypeError``. - -**Arguments:** - -+-----------------------------------------------+--------------+-------------------------------------------------------------+ -| Name | Type | Description | -+===============================================+==============+=============================================================+ -| [message=undefined] | String|Error | See :ref:`OAuthError#constructor`. | -+-----------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties={}] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.code=400] | Object | See :ref:`OAuthError#constructor`. | -+-----------------------------------------------+--------------+-------------------------------------------------------------+ -| [properties.name='unsupported_response_type'] | String | The error name used in responses generated from this error. | -+-----------------------------------------------+--------------+-------------------------------------------------------------+ - -**Return value:** - -A new instance of ``UnsupportedResponseTypeError``. - -**Remarks:** - -:: - - const err = new UnsupportedResponseTypeError(); - // err.message === 'Bad Request' - // err.code === 400 - // err.name === 'unsupported_response_type' - --------- - -.. _UnsupportedResponseTypeError#message: - -``message`` -=========== - -See :ref:`OAuthError#message `. - --------- - -.. _UnsupportedResponseTypeError#code: - -``code`` -======== - -Typically ``400``. See :ref:`OAuthError#code `. - --------- - -.. _UnsupportedResponseTypeError#inner: - -``inner`` -========= - -See :ref:`OAuthError#inner `. - --------- - -.. _UnsupportedResponseTypeError#name: - -``name`` -======== - -Typically ``'unsupported_response_type'``. See :ref:`OAuthError#name `. - diff --git a/docs/api/grant-types/abstract-grant-type.md b/docs/api/grant-types/abstract-grant-type.md new file mode 100644 index 00000000..081c97e3 --- /dev/null +++ b/docs/api/grant-types/abstract-grant-type.md @@ -0,0 +1,98 @@ + + +## AbstractGrantType +**Kind**: global class +**Classdesc#**: + +* [AbstractGrantType](#AbstractGrantType) + * [new AbstractGrantType(options)](#new_AbstractGrantType_new) + * [.generateAccessToken(client, user, scope)](#AbstractGrantType+generateAccessToken) ⇒ Promise.<string> + * [.generateRefreshToken()](#AbstractGrantType+generateRefreshToken) + * [.getAccessTokenExpiresAt()](#AbstractGrantType+getAccessTokenExpiresAt) + * [.getRefreshTokenExpiresAt()](#AbstractGrantType+getRefreshTokenExpiresAt) ⇒ Date + * [.getScope(request)](#AbstractGrantType+getScope) ⇒ string \| undefined + * [.validateScope(user, client, scope)](#AbstractGrantType+validateScope) ⇒ string + + + +### new AbstractGrantType(options) +**Throws**: + +- InvalidArgumentError if {options.accessTokenLifeTime} is missing +- InvalidArgumentError if {options.model} is missing + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | object | | | +| options.accessTokenLifetime | number | | access token lifetime in seconds | +| options.model | Model | | the model | +| options.refreshTokenLifetime | number | | | +| [options.alwaysIssueNewRefreshToken] | boolean | true | Always revoke the used refresh token and issue a new one for the `refresh_token` grant. | + + + +### abstractGrantType.generateAccessToken(client, user, scope) ⇒ Promise.<string> +Generate access token. +If the model implements `generateAccessToken` then +this implementation will be used. +Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) + +| Param | +| --- | +| client | +| user | +| scope | + + + +### abstractGrantType.generateRefreshToken() +Generate refresh token. + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) + + +### abstractGrantType.getAccessTokenExpiresAt() +Get access token expiration date. + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) + + +### abstractGrantType.getRefreshTokenExpiresAt() ⇒ Date +Get refresh token expiration date (now + refresh token lifetime) + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) + + +### abstractGrantType.getScope(request) ⇒ string \| undefined +Get scope from the request body. + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) + +| Param | Type | +| --- | --- | +| request | Request | + + + +### abstractGrantType.validateScope(user, client, scope) ⇒ string +Validate requested scope. +Delegates validation to the Model's `validateScope` method, +if the model implements this method. +Otherwise, treats given scope as valid. + +**Kind**: instance method of [AbstractGrantType](#AbstractGrantType) +**Returns**: string - the validated scope +**Throws**: + +- InvalidScopeError if the {Model#validateScope} method returned a falsy value + + +| Param | Type | +| --- | --- | +| user | object | +| client | ClientData | +| scope | string | + diff --git a/docs/api/grant-types/authorization-code-grant-type.md b/docs/api/grant-types/authorization-code-grant-type.md new file mode 100644 index 00000000..874dc7f0 --- /dev/null +++ b/docs/api/grant-types/authorization-code-grant-type.md @@ -0,0 +1,94 @@ + + +## AuthorizationCodeGrantType +**Kind**: global class + +* [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) + * [new AuthorizationCodeGrantType(options)](#new_AuthorizationCodeGrantType_new) + * [.handle(request, client)](#AuthorizationCodeGrantType+handle) + * [.getAuthorizationCode(request, client)](#AuthorizationCodeGrantType+getAuthorizationCode) ⇒ Promise.<{user}> + * [.validateRedirectUri(request, code)](#AuthorizationCodeGrantType+validateRedirectUri) + * [.revokeAuthorizationCode(code)](#AuthorizationCodeGrantType+revokeAuthorizationCode) + * [.saveToken(user, client, authorizationCode, requestedScope)](#AuthorizationCodeGrantType+saveToken) + + + +### new AuthorizationCodeGrantType(options) + +| Param | +| --- | +| options | + + + +### authorizationCodeGrantType.handle(request, client) +Handle authorization code grant. + +**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.3 + +| Param | Type | +| --- | --- | +| request | Request | +| client | ClientData | + + + +### authorizationCodeGrantType.getAuthorizationCode(request, client) ⇒ Promise.<{user}> +Get the authorization code. + +**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) + +| Param | Type | +| --- | --- | +| request | Request | +| client | ClientData | + + + +### authorizationCodeGrantType.validateRedirectUri(request, code) +Validate the redirect URI. + +"The authorization server MUST ensure that the redirect_uri parameter is +present if the redirect_uri parameter was included in the initial +authorization request as described in Section 4.1.1, and if included +ensure that their values are identical." + +**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.3 + +| Param | Type | +| --- | --- | +| request | Request | +| code | AuthorizationCodeData | + + + +### authorizationCodeGrantType.revokeAuthorizationCode(code) +Revoke the authorization code. + +"The authorization code MUST expire shortly after it is issued to mitigate +the risk of leaks. [...] If an authorization code is used more than once, +the authorization server MUST deny the request." + +**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-4.1.2 + +| Param | Type | +| --- | --- | +| code | AuthorizationCodeData | + + + +### authorizationCodeGrantType.saveToken(user, client, authorizationCode, requestedScope) +Save token. + +**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType) + +| Param | Type | +| --- | --- | +| user | object | +| client | ClientData | +| authorizationCode | string | +| requestedScope | string | + diff --git a/docs/api/grant-types/client-credentials-grant-type.md b/docs/api/grant-types/client-credentials-grant-type.md new file mode 100644 index 00000000..e31aa52b --- /dev/null +++ b/docs/api/grant-types/client-credentials-grant-type.md @@ -0,0 +1,31 @@ + + +## ClientCredentialsGrantType +Constructor. + +**Kind**: global class + +* [ClientCredentialsGrantType](#ClientCredentialsGrantType) + * [.handle()](#ClientCredentialsGrantType+handle) + * [.getUserFromClient()](#ClientCredentialsGrantType+getUserFromClient) + * [.saveToken()](#ClientCredentialsGrantType+saveToken) + + + +### clientCredentialsGrantType.handle() +Handle client credentials grant. + +**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-4.4.2 + + +### clientCredentialsGrantType.getUserFromClient() +Retrieve the user using client credentials. + +**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType) + + +### clientCredentialsGrantType.saveToken() +Save token. + +**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType) diff --git a/docs/api/grant-types/password-grant-type.md b/docs/api/grant-types/password-grant-type.md new file mode 100644 index 00000000..c3f13452 --- /dev/null +++ b/docs/api/grant-types/password-grant-type.md @@ -0,0 +1,33 @@ + + +## ~~PasswordGrantType~~ +***Deprecated*** + +Constructor. + +**Kind**: global class + +* ~~[PasswordGrantType](#PasswordGrantType)~~ + * [.handle()](#PasswordGrantType+handle) + * [.getUser()](#PasswordGrantType+getUser) + * [.saveToken()](#PasswordGrantType+saveToken) + + + +### passwordGrantType.handle() +Retrieve the user from the model using a username/password combination. + +**Kind**: instance method of [PasswordGrantType](#PasswordGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-4.3.2 + + +### passwordGrantType.getUser() +Get user using a username/password combination. + +**Kind**: instance method of [PasswordGrantType](#PasswordGrantType) + + +### passwordGrantType.saveToken() +Save token. + +**Kind**: instance method of [PasswordGrantType](#PasswordGrantType) diff --git a/docs/api/grant-types/refresh-token-grant-type.md b/docs/api/grant-types/refresh-token-grant-type.md new file mode 100644 index 00000000..5ccece94 --- /dev/null +++ b/docs/api/grant-types/refresh-token-grant-type.md @@ -0,0 +1,39 @@ + + +## RefreshTokenGrantType +Constructor. + +**Kind**: global class + +* [RefreshTokenGrantType](#RefreshTokenGrantType) + * [.handle()](#RefreshTokenGrantType+handle) + * [.getRefreshToken()](#RefreshTokenGrantType+getRefreshToken) + * [.revokeToken()](#RefreshTokenGrantType+revokeToken) + * [.saveToken()](#RefreshTokenGrantType+saveToken) + + + +### refreshTokenGrantType.handle() +Handle refresh token grant. + +**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-6 + + +### refreshTokenGrantType.getRefreshToken() +Get refresh token. + +**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType) + + +### refreshTokenGrantType.revokeToken() +Revoke the refresh token. + +**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType) +**See**: https://tools.ietf.org/html/rfc6749#section-6 + + +### refreshTokenGrantType.saveToken() +Save token. + +**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType) diff --git a/docs/api/handlers/authenticate-handler.md b/docs/api/handlers/authenticate-handler.md new file mode 100644 index 00000000..243c1ef9 --- /dev/null +++ b/docs/api/handlers/authenticate-handler.md @@ -0,0 +1,136 @@ + + +## AuthenticateHandler +**Kind**: global class + +* [AuthenticateHandler](#AuthenticateHandler) + * [new AuthenticateHandler(options)](#new_AuthenticateHandler_new) + * [.handle(request, response)](#AuthenticateHandler+handle) ⇒ Promise.<\*> + * [.getTokenFromRequest(request)](#AuthenticateHandler+getTokenFromRequest) + * [.getTokenFromRequestHeader(request)](#AuthenticateHandler+getTokenFromRequestHeader) + * [.getTokenFromRequestQuery(request)](#AuthenticateHandler+getTokenFromRequestQuery) + * [.getTokenFromRequestBody(request)](#AuthenticateHandler+getTokenFromRequestBody) + * [.getAccessToken(token)](#AuthenticateHandler+getAccessToken) + * [.validateAccessToken()](#AuthenticateHandler+validateAccessToken) + * [.verifyScope()](#AuthenticateHandler+verifyScope) + * [.updateResponse()](#AuthenticateHandler+updateResponse) + + + +### new AuthenticateHandler(options) +**Throws**: + +- InvalidArgumentError if {model} is missing or does not implement `getAccessToken` + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | object | | Server options. | +| options.model | Model | | The Model; this is always required. | +| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. | +| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. | +| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. | +| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. | + + + +### authenticateHandler.handle(request, response) ⇒ Promise.<\*> +Handles the authentication + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) + +| Param | Type | +| --- | --- | +| request | Request | +| response | Response | + + + +### authenticateHandler.getTokenFromRequest(request) +Get the token from the header or body, depending on the request. + +"Clients MUST NOT use more than one method to transmit the token in each request." + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) +**See**: {https://tools.ietf.org/html/rfc6750#section-2} + +| Param | Type | +| --- | --- | +| request | Request | + + + +### authenticateHandler.getTokenFromRequestHeader(request) +Get the token from the request header. + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) +**See**: {http://tools.ietf.org/html/rfc6750#section-2.1} + +| Param | Type | +| --- | --- | +| request | Request | + + + +### authenticateHandler.getTokenFromRequestQuery(request) +Get the token from the request query. + +"Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page +URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be +passed in HTTP message headers or message bodies for which confidentiality measures +are taken. Browsers, web servers, and other software may not adequately secure URLs +in the browser history, web server logs, and other data structures. If bearer tokens +are passed in page URLs, attackers might be able to steal them from the history data, +logs, or other unsecured locations." + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) +**See**: http://tools.ietf.org/html/rfc6750#section-2.3 + +| Param | Type | +| --- | --- | +| request | Request | + + + +### authenticateHandler.getTokenFromRequestBody(request) +Get the token from the request body. + +"The HTTP request method is one for which the request-body has defined semantics. +In particular, this means that the "GET" method MUST NOT be used." + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) +**See**: http://tools.ietf.org/html/rfc6750#section-2.2 + +| Param | Type | +| --- | --- | +| request | Request | + + + +### authenticateHandler.getAccessToken(token) +Get the access token from the model. + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) + +| Param | +| --- | +| token | + + + +### authenticateHandler.validateAccessToken() +Validate access token. + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) + + +### authenticateHandler.verifyScope() +Verify scope. + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) + + +### authenticateHandler.updateResponse() +Update response. + +**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler) diff --git a/docs/api/handlers/authorize-handler.md b/docs/api/handlers/authorize-handler.md new file mode 100644 index 00000000..b2b75ca5 --- /dev/null +++ b/docs/api/handlers/authorize-handler.md @@ -0,0 +1,142 @@ +## Classes + +
+
AuthorizeHandler
+

Constructor.

+
+
+ +## Constants + +
+
responseTypes
+

Response types.

+
+
+ + + +## AuthorizeHandler +Constructor. + +**Kind**: global class + +* [AuthorizeHandler](#AuthorizeHandler) + * [.handle()](#AuthorizeHandler+handle) + * [.generateAuthorizationCode()](#AuthorizeHandler+generateAuthorizationCode) + * [.getAuthorizationCodeLifetime()](#AuthorizeHandler+getAuthorizationCodeLifetime) + * [.getClient()](#AuthorizeHandler+getClient) + * [.validateScope()](#AuthorizeHandler+validateScope) + * [.getScope()](#AuthorizeHandler+getScope) + * [.getState()](#AuthorizeHandler+getState) + * [.getUser()](#AuthorizeHandler+getUser) + * [.getRedirectUri()](#AuthorizeHandler+getRedirectUri) + * [.saveAuthorizationCode()](#AuthorizeHandler+saveAuthorizationCode) + * [.getResponseType()](#AuthorizeHandler+getResponseType) + * [.buildSuccessRedirectUri()](#AuthorizeHandler+buildSuccessRedirectUri) + * [.buildErrorRedirectUri()](#AuthorizeHandler+buildErrorRedirectUri) + * [.updateResponse()](#AuthorizeHandler+updateResponse) + * [.getCodeChallengeMethod()](#AuthorizeHandler+getCodeChallengeMethod) + + + +### authorizeHandler.handle() +Authorize Handler. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.generateAuthorizationCode() +Generate authorization code. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getAuthorizationCodeLifetime() +Get authorization code lifetime. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getClient() +Get the client from the model. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.validateScope() +Validate requested scope. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getScope() +Get scope from the request. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getState() +Get state from the request. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getUser() +Get user by calling the authenticate middleware. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getRedirectUri() +Get redirect URI. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.saveAuthorizationCode() +Save authorization code. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getResponseType() +Get response type. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.buildSuccessRedirectUri() +Build a successful response that redirects the user-agent to the client-provided url. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.buildErrorRedirectUri() +Build an error response that redirects the user-agent to the client-provided url. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.updateResponse() +Update response with the redirect uri and the state parameter, if available. + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) + + +### authorizeHandler.getCodeChallengeMethod() +Get code challenge method from request or defaults to plain. +https://www.rfc-editor.org/rfc/rfc7636#section-4.3 + +**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler) +**Throws**: + +- InvalidRequestError if request contains unsupported code_challenge_method + (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) + + + +## responseTypes +Response types. + +**Kind**: global constant diff --git a/docs/api/handlers/token-handler.md b/docs/api/handlers/token-handler.md new file mode 100644 index 00000000..d0817a0d --- /dev/null +++ b/docs/api/handlers/token-handler.md @@ -0,0 +1,105 @@ +## Classes + +
+
TokenHandler
+

Constructor.

+
+
+ +## Constants + +
+
grantTypes
+

Grant types.

+
+
+ + + +## TokenHandler +Constructor. + +**Kind**: global class + +* [TokenHandler](#TokenHandler) + * [.handle()](#TokenHandler+handle) + * [.getClient()](#TokenHandler+getClient) + * [.getClientCredentials()](#TokenHandler+getClientCredentials) + * [.handleGrantType()](#TokenHandler+handleGrantType) + * [.getAccessTokenLifetime()](#TokenHandler+getAccessTokenLifetime) + * [.getRefreshTokenLifetime()](#TokenHandler+getRefreshTokenLifetime) + * [.getTokenType()](#TokenHandler+getTokenType) + * [.updateSuccessResponse()](#TokenHandler+updateSuccessResponse) + * [.updateErrorResponse()](#TokenHandler+updateErrorResponse) + * [.isClientAuthenticationRequired()](#TokenHandler+isClientAuthenticationRequired) + + + +### tokenHandler.handle() +Token Handler. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.getClient() +Get the client from the model. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.getClientCredentials() +Get client credentials. + +The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, +the `client_id` and `client_secret` can be embedded in the body. + +**Kind**: instance method of [TokenHandler](#TokenHandler) +**See**: https://tools.ietf.org/html/rfc6749#section-2.3.1 + + +### tokenHandler.handleGrantType() +Handle grant type. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.getAccessTokenLifetime() +Get access token lifetime. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.getRefreshTokenLifetime() +Get refresh token lifetime. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.getTokenType() +Get token type. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.updateSuccessResponse() +Update response when a token is generated. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.updateErrorResponse() +Update response when an error is thrown. + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +### tokenHandler.isClientAuthenticationRequired() +Given a grant type, check if client authentication is required + +**Kind**: instance method of [TokenHandler](#TokenHandler) + + +## grantTypes +Grant types. + +**Kind**: global constant diff --git a/docs/api/model.md b/docs/api/model.md new file mode 100644 index 00000000..0fc99984 --- /dev/null +++ b/docs/api/model.md @@ -0,0 +1,682 @@ +## Classes + +
+
Model
+

The Model implements the interface through +which some aspects of storage, retrieval and custom +validation are abstracted.

+

Each model function is resolved async by default. +This implies that async and sync model functions, +as well as generators, are supported.

+
+
+ +## Typedefs + +
+
AccessTokenData
+

An Object representing the access token and associated data. token.client and token.user can carry additional properties that will be ignored by oauth2-server.

+
+
RefreshTokenData
+

An Object representing the refresh token and associated data. token.client and token.user can carry additional properties that will be ignored by oauth2-server.

+
+
AuthorizationCodeData
+

An Object representing the authorization code and associated data. code.client and code.user can carry additional properties that will be ignored by oauth2-server.

+
+
ClientData
+

An Object representing the client and associated data.

+
+
+ + + +## Model +The Model implements the interface through +which some aspects of storage, retrieval and custom +validation are abstracted. + +Each model function is resolved async by default. +This implies that async and sync model functions, +as well as generators, are supported. + +**Kind**: global class + +* [Model](#Model) + * [new Model()](#new_Model_new) + * _instance_ + * [.getClient(clientId, clientSecret)](#Model+getClient) ⇒ [Promise.<ClientData>](#ClientData) + * [.saveToken(token, client, user)](#Model+saveToken) ⇒ Promise.<object> + * ~~[.getUser(username, password, [client])](#Model+getUser) ⇒ Promise.<(object\|null\|undefined\|false\|0)>~~ + * [.getUserFromClient(client)](#Model+getUserFromClient) ⇒ Promise.<object> + * [.getAccessToken(accessToken)](#Model+getAccessToken) ⇒ [Promise.<AccessTokenData>](#AccessTokenData) + * [.getRefreshToken(refreshToken)](#Model+getRefreshToken) ⇒ [Promise.<RefreshTokenData>](#RefreshTokenData) + * [.getAuthorizationCode(authorizationCode)](#Model+getAuthorizationCode) ⇒ [Promise.<AuthorizationCodeData>](#AuthorizationCodeData) + * [.saveAuthorizationCode(code, client, user)](#Model+saveAuthorizationCode) ⇒ Promise.<object> + * [.revokeToken(token)](#Model+revokeToken) ⇒ Promise.<boolean> + * [.revokeAuthorizationCode(code)](#Model+revokeAuthorizationCode) ⇒ Promise.<boolean> + * [.verifyScope(accessToken, scope)](#Model+verifyScope) ⇒ Promise.<boolean> + * [.generateAccessToken(client, user, scope)](#Model+generateAccessToken) ⇒ Promise.<string> + * [.generateRefreshToken(client, user, scope)](#Model+generateRefreshToken) ⇒ Promise.<string> + * [.generateAuthorizationCode(client, user, scope)](#Model+generateAuthorizationCode) ⇒ Promise.<string> + * [.validateScope(user, client, scope)](#Model+validateScope) ⇒ Promise.<boolean> + * [.validateRedirectUri(redirectUri, client)](#Model+validateRedirectUri) ⇒ Promise.<boolean> + * _static_ + * [.from(impl)](#Model.from) ⇒ [Model](#Model) + + + +### new Model() +**Example** +```js +const model = Model.from({ + getClient: () => { ... } +}) +``` + + +### model.getClient(clientId, clientSecret) ⇒ [Promise.<ClientData>](#ClientData) +Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. +This model function is **required** for all grant types. +**Invoked during:** + +- `authorization_code` grant +- `client_credentials` grant +- `refresh_token` grant +- `password` grant + +**Kind**: instance method of [Model](#Model) +**Fulfil**: [ClientData](#ClientData) - An `Object` representing the client and associated data, or a falsy value if no such client could be found. +**Reject**: Error - An Error type + +| Param | Type | Description | +| --- | --- | --- | +| clientId | string | The client id of the client to retrieve. | +| clientSecret | string | The client secret of the client to retrieve. Can be `null`. | + + + +### model.saveToken(token, client, user) ⇒ Promise.<object> +Invoked to save an access token and optionally a refresh token, depending on the grant type. +This model function is **required** for all grant types. + +**Invoked during:** +- `authorization_code` grant +- `client_credentials` grant +- `refresh_token` grant +- `password` grant + +If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. + +**Kind**: instance method of [Model](#Model) +**Fulfil**: {accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object} An `Object` representing the token(s) and associated data. + +| Param | Type | Description | +| --- | --- | --- | +| token | object | The token(s) to be saved. | +| token.accessToken | string | The access token to be saved. | +| token.accessTokenExpiresAt | Date | The expiry time of the access token. | +| token.refreshToken | string | The refresh token to be saved. | +| token.refreshTokenExpiresAt | Date | The expiry time of the refresh token. | +| token.scope | Array.<string> | The authorized scope of the token(s) | +| client | [ClientData](#ClientData) | The client associated with the token(s). | +| user | object | The user associated with the token(s). | + +**Example** +```js +function saveToken(token, client, user) { + // imaginary DB queries + let fns = [ + db.saveAccessToken({ + access_token: token.accessToken, + expires_at: token.accessTokenExpiresAt, + scope: token.scope, + client_id: client.id, + user_id: user.id + }), + db.saveRefreshToken({ + refresh_token: token.refreshToken, + expires_at: token.refreshTokenExpiresAt, + scope: token.scope, + client_id: client.id, + user_id: user.id + }) + ]; + return Promise.all(fns); + .spread(function(accessToken, refreshToken) { + return { + accessToken: accessToken.access_token, + accessTokenExpiresAt: accessToken.expires_at, + refreshToken: refreshToken.refresh_token, + refreshTokenExpiresAt: refreshToken.expires_at, + scope: accessToken.scope, + client: {id: accessToken.client_id}, + user: {id: accessToken.user_id} + }; + }); +} +``` + + +### ~~model.getUser(username, password, [client]) ⇒ Promise.<(object\|null\|undefined\|false\|0)>~~ +***Deprecated*** + +Invoked to retrieve a user using a username/password combination. +This model function is **required** if the `password` grant is used. +Please note, that password grant is considered unsafe. +It is still supported but marked deprecated. + +**Invoked during:** +- `password` grant + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<(object\|null\|undefined\|false\|0)> - An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + +| Param | Type | Description | +| --- | --- | --- | +| username | string | The username of the user to retrieve. | +| password | string | The user's password. | +| [client] | [ClientData](#ClientData) | The client. | + +**Example** +```js +function getUser(username, password) { + // imaginary DB query + return db.queryUser({username: username, password: password}); +} +``` + + +### model.getUserFromClient(client) ⇒ Promise.<object> +Invoked to retrieve the user associated with the specified client. +This model function is **required** if the `client_credentials` grant is used. + +**Invoked during:** +- `client_credentials` grant + +**Remarks:** + +`client` is the object previously obtained through `Model#getClient() `. + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<object> - An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + +| Param | Type | Description | +| --- | --- | --- | +| client | [ClientData](#ClientData) | The client to retrieve the associated user for. | + +**Example** +```js +function getUserFromClient(client) { + // imaginary DB query + return db.queryUser({id: client.user_id}); +} +``` + + +### model.getAccessToken(accessToken) ⇒ [Promise.<AccessTokenData>](#AccessTokenData) +Invoked to retrieve an existing access token, including associated data, that has previously been saved through `Model#saveToken() `. +This model function is **required** if `OAuth2Server#authenticate() ` is used. + +**Invoked during:** +- request authentication + +**Kind**: instance method of [Model](#Model) +**Returns**: [Promise.<AccessTokenData>](#AccessTokenData) - the object, containing the data, stored with the access token + +| Param | Type | Description | +| --- | --- | --- | +| accessToken | string | The access token to retrieve. | + +**Example** +```js +function getAccessToken(accessToken) { + // imaginary DB queries + return db.queryAccessToken({access_token: accessToken}) + .then(function(token) { + return Promise.all([ + token, + db.queryClient({id: token.client_id}), + db.queryUser({id: token.user_id}) + ]); + }) + .spread(function(token, client, user) { + return { + accessToken: token.access_token, + accessTokenExpiresAt: token.expires_at, + scope: token.scope, + client: client, // with 'id' property + user: user + }; + }); +} +``` + + +### model.getRefreshToken(refreshToken) ⇒ [Promise.<RefreshTokenData>](#RefreshTokenData) +Invoked to retrieve an existing refresh token previously saved through `Model#saveToken() `. +This model function is **required** if the `refresh_token` grant is used. +**Invoked during:** +- `refresh_token` grant + +**Kind**: instance method of [Model](#Model) +**Returns**: [Promise.<RefreshTokenData>](#RefreshTokenData) - An `Object` representing the refresh token and associated data. + +| Param | Type | Description | +| --- | --- | --- | +| refreshToken | string | The access token to retrieve. | + +**Example** +```js +function getRefreshToken(refreshToken) { + // imaginary DB queries + return db.queryRefreshToken({refresh_token: refreshToken}) + .then(function(token) { + return Promise.all([ + token, + db.queryClient({id: token.client_id}), + db.queryUser({id: token.user_id}) + ]); + }) + .spread(function(token, client, user) { + return { + refreshToken: token.refresh_token, + refreshTokenExpiresAt: token.expires_at, + scope: token.scope, + client: client, // with 'id' property + user: user + }; + }); +} +``` + + +### model.getAuthorizationCode(authorizationCode) ⇒ [Promise.<AuthorizationCodeData>](#AuthorizationCodeData) +Invoked to retrieve an existing authorization code previously saved through `Model#saveAuthorizationCode() `. +This model function is **required** if the `authorization_code` grant is used. +**Invoked during:** +- `authorization_code` grant + +**Kind**: instance method of [Model](#Model) +**Returns**: [Promise.<AuthorizationCodeData>](#AuthorizationCodeData) - An `Object` representing the authorization code and associated data. + +| Param | Type | Description | +| --- | --- | --- | +| authorizationCode | string | The authorization code to retrieve. | + +**Example** +```js +function getAuthorizationCode(authorizationCode) { + // imaginary DB queries + return db.queryAuthorizationCode({authorization_code: authorizationCode}) + .then(function(code) { + return Promise.all([ + code, + db.queryClient({id: code.client_id}), + db.queryUser({id: code.user_id}) + ]); + }) + .spread(function(code, client, user) { + return { + authorizationCode: code.authorization_code, + expiresAt: code.expires_at, + redirectUri: code.redirect_uri, + scope: code.scope, + client: client, // with 'id' property + user: user + }; + }); +} +``` + + +### model.saveAuthorizationCode(code, client, user) ⇒ Promise.<object> +Invoked to save an authorization code. +This model function is **required** if the `authorization_code` grant is used. + +**Invoked during:** +- `authorization_code` grant + +**Kind**: instance method of [Model](#Model) +**Fulfil**: { authorizationCode: string, expiresAt: Date, redirectUri: string,scope: string[],client: ClientData,user: object} An `Object` representing the authorization code and associated data. `code.client` and `code.user` can carry additional properties that will be ignored by *oauth2-server*. + +| Param | Type | Description | +| --- | --- | --- | +| code | object | The code to be saved. | +| code.authorizationCode | string | The authorization code to be saved. | +| code.expiresAt | Date | The expiry time of the authorization code. | +| code.redirectUri | string | The redirect URI associated with the authorization code. | +| code.scope | Array.<string> | The authorized scope of the authorization code. | +| client | [ClientData](#ClientData) | The client associated with the authorization code. | +| user | object | The user associated with the authorization code. | + +**Example** +```js +function saveAuthorizationCode(code, client, user) { + // imaginary DB queries + let authCode = { + authorization_code: code.authorizationCode, + expires_at: code.expiresAt, + redirect_uri: code.redirectUri, + scope: code.scope, + client_id: client.id, + user_id: user.id + }; + return db.saveAuthorizationCode(authCode) + .then(function(authorizationCode) { + return { + authorizationCode: authorizationCode.authorization_code, + expiresAt: authorizationCode.expires_at, + redirectUri: authorizationCode.redirect_uri, + scope: authorizationCode.scope, + client: {id: authorizationCode.client_id}, + user: {id: authorizationCode.user_id} + }; + }); +} +``` + + +### model.revokeToken(token) ⇒ Promise.<boolean> +Invoked to revoke a refresh token. +This model function is **required** if the `refresh_token` grant is used. +**Invoked during:** +- `refresh_token` grant + +**Remarks:** +`token` is the refresh token object previously obtained through `Model#getRefreshToken() `. + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<boolean> - Return `true` if the revocation was successful or `false` if the refresh token could not be found. + +| Param | Type | +| --- | --- | +| token | [RefreshTokenData](#RefreshTokenData) | + +**Example** +```js +function revokeToken(token) { + // imaginary DB queries + return db.deleteRefreshToken({refresh_token: token.refreshToken}) + .then(function(refreshToken) { + return !!refreshToken; + }); +} +``` + + +### model.revokeAuthorizationCode(code) ⇒ Promise.<boolean> +Invoked to revoke an authorization code. +This model function is **required** if the `authorization_code` grant is used. + +**Invoked during:** +- `authorization_code` grant + +**Remarks:** +`code` is the authorization code object previously obtained through [getAuthorizationCode](#Model+getAuthorizationCode). + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<boolean> - Return `true` if the revocation was successful or `false` if the authorization code could not be found. + +| Param | Type | +| --- | --- | +| code | [AuthorizationCodeData](#AuthorizationCodeData) | + + + +### model.verifyScope(accessToken, scope) ⇒ Promise.<boolean> +Invoked during request authentication to check if the provided access token was authorized the requested scopes. + +This model function is **required** if scopes are used with `OAuth2Server#authenticate() ` +but it's never called, if you provide your own `authenticateHandler` to the options. + +**Invoked during:** +- request authentication + +**Remarks:** +- `token` is the access token object previously obtained through `Model#getAccessToken() `. +- `scope` is the required scope as given to `OAuth2Server#authenticate() ` as `options.scope`. + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<boolean> - Returns `true` if the access token passes, `false` otherwise. + +| Param | Type | Description | +| --- | --- | --- | +| accessToken | [AccessTokenData](#AccessTokenData) | | +| scope | Array.<string> | The required scopes. | + +**Example** +```js +function verifyScope(token, requestedScopes) { + if (!token.scope) { + return false; + } + let authorizedScopes = token.scope; + return requestedScopes.every(s => authorizedScopes.includes(s)); +} +``` + + +### model.generateAccessToken(client, user, scope) ⇒ Promise.<string> +Invoked to generate a new access token. +This model function is **optional**. + +If not implemented, a default handler is used that generates access tokens consisting of 40 characters in the range of `a..z0..9`. +[RFC 6749 Appendix A.12](https://www.rfc-editor.org/rfc/rfc6749#appendix-A.12>) specifies that access tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + +**Invoked during:** +- `authorization_code` grant +- `client_credentials` grant +- `refresh_token` grant +- `password` grant + +**Remarks:** +- `client` is the object previously obtained through `Model#getClient() `. +- `user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant). + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<string> - A `String` to be used as access token. + +| Param | Type | Description | +| --- | --- | --- | +| client | object | The client the access token is generated for | +| user | object | The user the access token is generated for. | +| scope | Array.<string> | The scopes associated with the token. Can be `null` | + + + +### model.generateRefreshToken(client, user, scope) ⇒ Promise.<string> +Invoked to generate a new refresh token. + +This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of `a..z0..9`. +[RFC 6749 Appendix A.17](https://www.rfc-editor.org/6749#appendix-A.17) specifies that refresh tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + +**Invoked during:** + +- `authorization_code` grant +- `refresh_token` grant +- `password` grant + +**Remarks:** + +`client` is the object previously obtained through `Model#getClient() `. + +`user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant). + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<string> - A `String` to be used as refresh token. + +| Param | Type | Description | +| --- | --- | --- | +| client | object | The client the refresh token is generated for | +| user | object | The user the refresh token is generated for. | +| scope | Array.<string> | The scopes associated with the refresh token. Can be `null` | + + + +### model.generateAuthorizationCode(client, user, scope) ⇒ Promise.<string> +Invoked to generate a new authorization code. +This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of `a..z0..9`. +[RFC 6749 Appendix A.11](https://www.rfc-editor.org/6749#appendix-A.11) specifies that authorization codes must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + +**Invoked during:** +- `authorization_code` grant +>` + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<string> - A `String` to be used as authorization code. + +| Param | Type | Description | +| --- | --- | --- | +| client | object | The client the authorization code is generated for. | +| user | object | The user the authorization code is generated for. | +| scope | Array.<string> | The scopes associated with the authorization code. Can be `null`. | + + + +### model.validateScope(user, client, scope) ⇒ Promise.<boolean> +Invoked to check if the requested `scope` is valid for a particular `client`/`user` combination. + +This model function is **optional**. If not implemented, any scope is accepted. + +**Invoked during:** + +- `authorization_code` grant +- `client_credentials` grant +- `password` grant + +**Remarks:** + +`user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant) or `Model#getUser() ` (password grant). + +`client` is the object previously obtained through `Model#getClient ` (all grants). + +You can decide yourself whether you want to reject or accept partially valid scopes by simply filtering out invalid scopes and returning only the valid ones. + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<boolean> - Validated scopes to be used or a falsy value to reject the requested scopes. + +| Param | Type | Description | +| --- | --- | --- | +| user | object | The associated user. | +| client | [ClientData](#ClientData) | The associated client. | +| scope | Array.<string> | The scopes to validate. | + +**Example** +```js +// To reject invalid or only partially valid scopes: +const VALID_SCOPES = ['read', 'write']; +function validateScope(user, client, scope) { + if (!scope.every(s => VALID_SCOPES.indexOf(s) >= 0)) { + return false; + } + return scope; +} +``` +**Example** +```js +// To accept partially valid scopes: +const VALID_SCOPES = ['read', 'write']; +function validateScope(user, client, scope) { + return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); +} +``` + + +### model.validateRedirectUri(redirectUri, client) ⇒ Promise.<boolean> +Invoked to check if the provided `redirectUri` is valid for a particular `client`. +This model function is **optional**. If not implemented, the `redirectUri` should be included in the provided `redirectUris` of the client. + +**Invoked during:** +- `authorization_code` grant + +**Remarks:** +When implementing this method you should take care of possible security risks related to `redirectUri`. +See: https://datatracker.ietf.org/doc/html/rfc6819 +(Section-5.2.3.5 is implemented by default). + +**Kind**: instance method of [Model](#Model) +**Returns**: Promise.<boolean> - Returns `true` if the `redirectUri` is valid, `false` otherwise. + +| Param | Type | Description | +| --- | --- | --- | +| redirectUri | string | The redirect URI to validate | +| client | object | The associated client. | + + + +### Model.from(impl) ⇒ [Model](#Model) +Factory function to create a model form your implementation. + +**Kind**: static method of [Model](#Model) +**Returns**: [Model](#Model) - the model instance. + +| Param | Type | Description | +| --- | --- | --- | +| impl | object | an object containing your model function implementations | + + + +## AccessTokenData +An `Object` representing the access token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*. + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| accessToken | string | The access token passed to `getAccessToken()` | +| accessTokenExpiresAt | Date | The expiry time of the access token. | +| scope | Array.<string> | The authorized scope of the access token. | +| client | object | The client associated with the access token. | +| client.id | string | A unique string identifying the client. | +| user | object | The user associated with the access token. | + + + +## RefreshTokenData +An `Object` representing the refresh token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*. + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| refreshToken | string | The refresh token passed to `getRefreshToken()` | +| refreshTokenExpiresAt | Date | The expiry time of the refresh token. | +| scope | Array.<string> | The authorized scope of the refresh token. | +| client | [ClientData](#ClientData) | The client associated with the refresh token. | +| user | object | The user associated with the access token. | + + + +## AuthorizationCodeData +An `Object` representing the authorization code and associated data. `code.client` and `code.user` can carry additional properties that will be ignored by *oauth2-server*. + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| code | string | The authorization code passed to `getAuthorizationCode()`. | +| expiresAt | Date | The expiry time of the authorization code. | +| redirectUri | string | The redirect URI of the authorization code. | +| scope | Array.<string> | The authorized scope of the authorization code. | +| client | [ClientData](#ClientData) | The client associated with the authorization code. | +| user | object | The user associated with the access token. | + + + +## ClientData +An `Object` representing the client and associated data. + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| id | string | The authorization code passed to `getAuthorizationCode()`. | +| redirectUris | Array.<string> | Redirect URIs allowed for the client. Required for the `authorization_code` grant. | +| grants | Array.<string> | Grant types allowed for the client. | +| accessTokenLifetime | number | Client-specific lifetime of generated access tokens in seconds. | +| refreshTokenLifetime | number | Client-specific lifetime of generated refresh tokens in seconds. | + diff --git a/docs/api/models/token-model.md b/docs/api/models/token-model.md new file mode 100644 index 00000000..fec92ad2 --- /dev/null +++ b/docs/api/models/token-model.md @@ -0,0 +1,13 @@ + + +## TokenModel +**Kind**: global class + + +### new TokenModel(data, options) + +| Param | +| --- | +| data | +| options | + diff --git a/docs/api/oauth2-server.rst b/docs/api/oauth2-server.rst deleted file mode 100644 index 2cb6cda4..00000000 --- a/docs/api/oauth2-server.rst +++ /dev/null @@ -1,290 +0,0 @@ -============== - OAuth2Server -============== - -Represents an OAuth2 server instance. - -:: - - const OAuth2Server = require('@node-oauth/oauth2-server'); - --------- - -.. _OAuth2Server#constructor: - -``new OAuth2Server(options)`` -============================= - -Instantiates ``OAuth2Server`` using the supplied model. - -**Arguments:** - -+---------------+--------+---------------------------------+ -| Name | Type | Description | -+===============+========+=================================+ -| options | Object | Server options. | -+---------------+--------+---------------------------------+ -| options.model | Object | The :doc:`Model `. | -+---------------+--------+---------------------------------+ - -**Return value:** - -A new ``OAuth2Server`` instance. - -**Remarks:** - -Any valid option for :ref:`OAuth2Server#authenticate() `, :ref:`OAuth2Server#authorize() ` and :ref:`OAuth2Server#token() ` can be passed to the constructor as well. The supplied options will be used as default for the other methods. - -Basic usage: - -:: - - const oauth = new OAuth2Server({ - model: require('./model') - }); - -Advanced example with additional options: - -:: - - const oauth = new OAuth2Server({ - model: require('./model'), - allowBearerTokensInQueryString: true, - accessTokenLifetime: 4 * 60 * 60 - }); - --------- - -.. _OAuth2Server#authenticate: - -``authenticate(request, response, [options])`` -========================================================== - -Authenticates a request. - -**Arguments:** - -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| Name | Type | Description | -+================================================+=================+=======================================================================+ -| request | :doc:`request` | Request object. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| response | :doc:`response` | Response object. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options={}] | Object | Handler options. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options.scope=undefined] | String[] | The scope(s) to authenticate. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options.addAcceptedScopesHeader=true] | Boolean | Set the ``X-Accepted-OAuth-Scopes`` HTTP header on response objects. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options.addAuthorizedScopesHeader=true] | Boolean | Set the ``X-OAuth-Scopes`` HTTP header on response objects. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options.allowBearerTokensInQueryString=false] | Boolean | Allow clients to pass bearer tokens in the query string of a request. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ - -**Return value:** - -A ``Promise`` that resolves to the access token object returned from :ref:`Model#getAccessToken() `. -In case of an error, the promise rejects with one of the error types derived from :doc:`/api/errors/oauth-error`. - -Possible errors include but are not limited to: - -:doc:`/api/errors/unauthorized-request-error`: - The protected resource request failed authentication. - -**Remarks:** - -:: - - const oauth = new OAuth2Server({model: ...}); - - function authenticateHandler(options) { - return function(req, res, next) { - let request = new Request(req); - let response = new Response(res); - return oauth.authenticate(request, response, options) - .then(function(token) { - res.locals.oauth = {token: token}; - next(); - }) - .catch(function(err) { - // handle error condition - }); - } - } - --------- - -.. _OAuth2Server#authorize: - -``authorize(request, response, [options])`` -======================================================= - -Authorizes a token request. - -**Arguments:** - -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| Name | Type | Description | -+=========================================+=================+=============================================================================+ -| request | :doc:`request` | Request object. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [request.query.allowed=undefined] | String | ``'false'`` to deny the authorization request (see remarks section). | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| response | :doc:`response` | Response object. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options={}] | Object | Handler options. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options.authenticateHandler=undefined] | Object | The authenticate handler (see remarks section). | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options.allowEmptyState=false] | Boolean | Allow clients to specify an empty ``state``. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options.authorizationCodeLifetime=300] | Number | Lifetime of generated authorization codes in seconds (default = 5 minutes). | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ - -**Return value:** - -A ``Promise`` that resolves to the authorization code object returned from :ref:`Model#saveAuthorizationCode() `. -In case of an error, the promise rejects with one of the error types derived from :doc:`/api/errors/oauth-error`. - -Possible errors include but are not limited to: - -:doc:`/api/errors/access-denied-error` - The resource owner denied the access request (i.e. ``request.query.allow`` was ``'false'``). - -**Remarks:** - -If ``request.query.allowed`` equals the string ``'false'`` the access request is denied and the returned promise is rejected with an :doc:`/api/errors/access-denied-error`. - -In order to retrieve the user associated with the request, ``options.authenticateHandler`` should be supplied. -The ``authenticateHandler`` has to be an object implementing a ``handle(request, response)`` function that returns a user object. -If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. - -:: - - let authenticateHandler = { - handle: function(request, response) { - return /* get authenticated user */; - } - }; - -When working with a session-based login mechanism, the handler can simply look like this: - -:: - - let authenticateHandler = { - handle: function(request, response) { - return request.session.user; - } - }; - -.. todo:: Move ``authenticateHandler`` to it's own section. - -:: - - const oauth = new OAuth2Server({model: ...}); - - function authorizeHandler(options) { - return function(req, res, next) { - let request = new Request(req); - let response = new Response(res); - return oauth.authorize(request, response, options) - .then(function(code) { - res.locals.oauth = {code: code}; - next(); - }) - .catch(function(err) { - // handle error condition - }); - } - } - --------- - -.. _OAuth2Server#token: - -``token(request, response, [options])`` -=================================================== - -Retrieves a new token for an authorized token request. - -**Arguments:** - -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| Name | Type | Description | -+==============================================+=================+===========================================================================================+ -| request | :doc:`request` | Request object. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| response | :doc:`response` | Response object. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options={}] | Object | Handler options. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.accessTokenLifetime=3600] | Number | Lifetime of generated access tokens in seconds (default = 1 hour). | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.refreshTokenLifetime=1209600] | Number | Lifetime of generated refresh tokens in seconds (default = 2 weeks). | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.allowExtendedTokenAttributes=false] | Boolean | Allow extended attributes to be set on the returned token (see remarks section). | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.requireClientAuthentication={}] | Object | Require a client secret (see remarks section). Defaults to ``true`` for all grant types. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.alwaysIssueNewRefreshToken=true] | Boolean | Always revoke the used refresh token and issue a new one for the ``refresh_token`` grant. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [options.extendedGrantTypes={}] | Object | Additional supported grant types. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ - -**Return value:** - -A ``Promise`` that resolves to the token object returned from :ref:`Model#saveToken() `. -In case of an error, the promise rejects with one of the error types derived from :doc:`/api/errors/oauth-error`. - -Possible errors include but are not limited to: - -:doc:`/api/errors/invalid-grant-error`: - The access token request was invalid or not authorized. - -**Remarks:** - -If ``options.allowExtendedTokenAttributes`` is ``true`` any additional properties set on the object returned from :ref:`Model#saveToken() ` are copied to the token response sent to the client. - -By default all grant types require the client to send it's ``client_secret`` with the token request. ``options.requireClientAuthentication`` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to ``true`` or ``false``. Possible keys for the object include all supported values for the token request's ``grant_type`` field (``authorization_code``, ``client_credentials``, ``password`` and ``refresh_token``). Grants that are not specified default to ``true`` which enables verification of the ``client_secret``. - -:: - - let options = { - // ... - // Allow token requests using the password grant to not include a client_secret. - requireClientAuthentication: {password: false} - }; - -``options.extendedGrantTypes`` is an object mapping extension grant URIs to handler types, for example: - -:: - - let options = { - // ... - extendedGrantTypes: { - 'urn:foo:bar:baz': MyGrantType - } - }; - -For information on how to implement a handler for a custom grant type see :doc:`/misc/extension-grants`. - -:: - - const oauth = new OAuth2Server({model: ...}); - - function tokenHandler(options) { - return function(req, res, next) { - let request = new Request(req); - let response = new Response(res); - return oauth.token(request, response, options) - .then(function(code) { - res.locals.oauth = {token: token}; - next(); - }) - .catch(function(err) { - // handle error condition - }); - } - } - diff --git a/docs/api/pkce/pkce.md b/docs/api/pkce/pkce.md new file mode 100644 index 00000000..c34e2c20 --- /dev/null +++ b/docs/api/pkce/pkce.md @@ -0,0 +1,64 @@ + + +## pkce + +* [pkce](#module_pkce) + * [~getHashForCodeChallenge(method, verifier)](#module_pkce..getHashForCodeChallenge) ⇒ String \| undefined + * [~codeChallengeMatchesABNF(codeChallenge)](#module_pkce..codeChallengeMatchesABNF) ⇒ Boolean + * [~isPKCERequest(grantType, codeVerifier)](#module_pkce..isPKCERequest) ⇒ boolean + * [~isValidMethod(method)](#module_pkce..isValidMethod) ⇒ boolean + + + +### pkce~getHashForCodeChallenge(method, verifier) ⇒ String \| undefined +Return hash for code-challenge method-type. + +**Kind**: inner method of [pkce](#module_pkce) + +| Param | Type | Description | +| --- | --- | --- | +| method | String | the code challenge method | +| verifier | String | the code_verifier | + + + +### pkce~codeChallengeMatchesABNF(codeChallenge) ⇒ Boolean +Matches a code verifier (or code challenge) against the following criteria: + +code-verifier = 43*128unreserved +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +ALPHA = %x41-5A / %x61-7A +DIGIT = %x30-39 + +**Kind**: inner method of [pkce](#module_pkce) +**See:**: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + +| Param | Type | +| --- | --- | +| codeChallenge | String | + + + +### pkce~isPKCERequest(grantType, codeVerifier) ⇒ boolean +Check if the request is a PCKE request. We assume PKCE if grant type is +'authorization_code' and code verifier is present. + +**Kind**: inner method of [pkce](#module_pkce) + +| Param | Type | +| --- | --- | +| grantType | String | +| codeVerifier | String | + + + +### pkce~isValidMethod(method) ⇒ boolean +Checks if the code challenge method is one of the supported methods +'sha256' or 'plain' + +**Kind**: inner method of [pkce](#module_pkce) + +| Param | Type | +| --- | --- | +| method | String | + diff --git a/docs/api/request.md b/docs/api/request.md new file mode 100644 index 00000000..8360100f --- /dev/null +++ b/docs/api/request.md @@ -0,0 +1,61 @@ + + +## Request +Wrapper for webserver's request. +Used to decouple this package from the webserver's +request signature. + +**Kind**: global class + +* [Request](#Request) + * [new Request(headers, method, query, [body], ...otherOptions)](#new_Request_new) + * [.get(field)](#Request+get) ⇒ string + * [.is(...types)](#Request+is) ⇒ boolean + + + +### new Request(headers, method, query, [body], ...otherOptions) +Creates a new request instance + +**Throws**: + +- InvalidArgumentError if one of headers, method or query are missing. + + +| Param | Type | Description | +| --- | --- | --- | +| headers | object | key-value object of headers | +| method | string | the HTTP method | +| query | object | key-value object of query parameters | +| [body] | object | optional key-value object of body parameters | +| ...otherOptions | object | any other properties that should be assigned to the request by your webserver | + +**Example** +```js +function (req, res, next) { + // most webservers follow a similar structure + const response = new Request(req); +} +``` + + +### request.get(field) ⇒ string +Get a request header (case-insensitive). + +**Kind**: instance method of [Request](#Request) + +| Param | Type | +| --- | --- | +| field | String | + + + +### request.is(...types) ⇒ boolean +Check if the content-type matches any of the given mime types. + +**Kind**: instance method of [Request](#Request) + +| Param | Type | +| --- | --- | +| ...types | Array.<string> | + diff --git a/docs/api/request.rst b/docs/api/request.rst deleted file mode 100644 index 7d5f4cad..00000000 --- a/docs/api/request.rst +++ /dev/null @@ -1,134 +0,0 @@ -========= - Request -========= - -Represents an incoming HTTP request. - -:: - - const Request = require('@node-oauth/oauth2-server').Request; - --------- - -.. _Request#constructor: - -``new Request(options)`` -======================== - -Instantiates ``Request`` using the supplied options. - -**Arguments:** - -+-------------------+--------+--------------------------------------------------------+ -| Name | Type | Description | -+===================+========+========================================================+ -| options | Object | Request options. | -+-------------------+--------+--------------------------------------------------------+ -| options.method | String | The HTTP method of the request. | -+-------------------+--------+--------------------------------------------------------+ -| options.query | Object | The request's query string parameters. | -+-------------------+--------+--------------------------------------------------------+ -| options.headers | Object | The request's HTTP header fields. | -+-------------------+--------+--------------------------------------------------------+ -| [options.body={}] | Object | Key-value pairs of data submitted in the request body. | -+-------------------+--------+--------------------------------------------------------+ - -All additional own properties are copied to the new ``Request`` object as well. - -**Return value:** - -A new ``Request`` instance. - -**Remarks:** - -The names of HTTP header fields passed in as ``options.headers`` are converted to lower case. - -To convert `Express' request`_ to a ``Request`` simply pass ``req`` as ``options``: - -.. _Express' request: https://expressjs.com/en/4x/api.html#req - -:: - - function(req, res, next) { - let request = new Request(req); - // ... - } - --------- - -.. _Request#get: - -``get(field)`` -============== - -Returns the specified HTTP header field. The match is case-insensitive. - -**Arguments:** - -+-------+--------+------------------------+ -| Name | Type | Description | -+=======+========+========================+ -| field | String | The header field name. | -+-------+--------+------------------------+ - -**Return value:** - -The value of the header field or ``undefined`` if the field does not exist. - --------- - -.. _Request#is: - -``is(types)`` -============= - -Checks if the request's ``Content-Type`` HTTP header matches any of the given MIME types. - -**Arguments:** - -+-------+----------------------+-----------------------------------+ -| Name | Type | Description | -+=======+======================+===================================+ -| types | Array|String | The MIME type(s) to test against. | -+-------+----------------------+-----------------------------------+ - -**Return value:** - -Returns the matching MIME type or ``false`` if there was no match. - --------- - -.. _Request#method: - -``method`` -========== - -The HTTP method of the request (``'GET'``, ``'POST'``, ``'PUT'``, ...). - --------- - -.. _Request#query: - -``query`` -========= - -The request's query string parameters. - --------- - -.. _Request#headers: - -``headers`` -=========== - -The request's HTTP header fields. Prefer :ref:`Request#get() ` over accessing this object directly. - --------- - -.. _Request#body: - -``body`` -======== - -Key-value pairs of data submitted in the request body. - diff --git a/docs/api/response-types/code-response-type.md b/docs/api/response-types/code-response-type.md new file mode 100644 index 00000000..1142f18b --- /dev/null +++ b/docs/api/response-types/code-response-type.md @@ -0,0 +1,34 @@ + + +## CodeResponseType +**Kind**: global class + +* [CodeResponseType](#CodeResponseType) + * [new CodeResponseType(code)](#new_CodeResponseType_new) + * [.buildRedirectUri(redirectUri)](#CodeResponseType+buildRedirectUri) ⇒ UrlWithParsedQuery + + + +### new CodeResponseType(code) +**Throws**: + +- InvalidArgumentError if {code} is missing + + +| Param | +| --- | +| code | + + + +### codeResponseType.buildRedirectUri(redirectUri) ⇒ UrlWithParsedQuery +**Kind**: instance method of [CodeResponseType](#CodeResponseType) +**Throws**: + +- InvalidArgumentError if redirectUri is missing + + +| Param | +| --- | +| redirectUri | + diff --git a/docs/api/response-types/token-response-type.md b/docs/api/response-types/token-response-type.md new file mode 100644 index 00000000..ee57ef7f --- /dev/null +++ b/docs/api/response-types/token-response-type.md @@ -0,0 +1,4 @@ + + +## TokenResponseType +**Kind**: global class diff --git a/docs/api/response.md b/docs/api/response.md new file mode 100644 index 00000000..e0b04020 --- /dev/null +++ b/docs/api/response.md @@ -0,0 +1,69 @@ + + +## Response +Wrapper for webserver's response object. +Used to decouple this package from the webserver's +response signature. + +**Kind**: global class + +* [Response](#Response) + * [new Response(headers, method, [body], ...otherOptions)](#new_Response_new) + * [.get(field)](#Response+get) ⇒ string \| undefined + * [.redirect(url)](#Response+redirect) + * [.set(field, value)](#Response+set) + + + +### new Response(headers, method, [body], ...otherOptions) +Create a new Response instance. + + +| Param | Type | Description | +| --- | --- | --- | +| headers | object | key-value object of headers | +| method | string | the HTTP method | +| [body] | object | optional key-value object of body parameters | +| ...otherOptions | object | any other properties that should be assigned to the request by your webserver | + +**Example** +```js +function (req, res, next) { + // most webservers follow a similar structure + const response = new Response(res); +} +``` + + +### response.get(field) ⇒ string \| undefined +Get a response header. + +**Kind**: instance method of [Response](#Response) + +| Param | Type | Description | +| --- | --- | --- | +| field | string | the field to access, case-insensitive | + + + +### response.redirect(url) +Redirect response. + +**Kind**: instance method of [Response](#Response) + +| Param | Type | Description | +| --- | --- | --- | +| url | string | the url to redirect to | + + + +### response.set(field, value) +Set a response header. + +**Kind**: instance method of [Response](#Response) + +| Param | Type | Description | +| --- | --- | --- | +| field | string | the name of the header field, case-insensitive | +| value | string | the new value of the header field | + diff --git a/docs/api/response.rst b/docs/api/response.rst deleted file mode 100644 index 2c5d3326..00000000 --- a/docs/api/response.rst +++ /dev/null @@ -1,148 +0,0 @@ -========== - Response -========== - -Represents an outgoing HTTP response. - -:: - - const Response = require('@node-oauth/oauth2-server').Response; - --------- - -.. _Response#constructor: - -``new Response(options)`` -========================= - -Instantiates ``Response`` using the supplied options. - -**Arguments:** - -+-------------------+--------+---------------------------------------------------------------+ -| Name | Type | Description | -+===================+========+===============================================================+ -| options | Object | Response options. | -+-------------------+--------+---------------------------------------------------------------+ -| options.headers | Object | The response's HTTP header fields. | -+-------------------+--------+---------------------------------------------------------------+ -| [options.body={}] | Object | Key-value pairs of data to be submitted in the response body. | -+-------------------+--------+---------------------------------------------------------------+ - -All additional own properties are copied to the new ``Response`` object as well. - -**Return value:** - -A new ``Response`` instance. - -**Remarks:** - -The names of HTTP header fields passed in as ``options.headers`` are converted to lower case. - -To convert `Express' response`_ to a ``Response`` simply pass ``res`` as ``options``: - -.. _Express' response: https://expressjs.com/en/4x/api.html#res - -:: - - function(req, res, next) { - let response = new Response(res); - // ... - } - --------- - -.. _Response#get: - -``get(field)`` -============== - -Returns the specified HTTP header field. The match is case-insensitive. - -**Arguments:** - -+-------+--------+------------------------+ -| Name | Type | Description | -+=======+========+========================+ -| field | String | The header field name. | -+-------+--------+------------------------+ - -**Return value:** - -The value of the header field or ``undefined`` if the field does not exist. - --------- - -.. _Response#set: - -``set(field, value)`` -===================== - -Sets the specified HTTP header field. The match is case-insensitive. - -**Arguments:** - -+-------+--------+-------------------------+ -| Name | Type | Description | -+=======+========+=========================+ -| field | String | The header field name. | -+-------+--------+-------------------------+ -| value | String | The header field value. | -+-------+--------+-------------------------+ - -**Return value:** - -None. - --------- - -.. _Response#redirect: - -``redirect(url)`` -================= - -Redirects to the specified URL using ``302 Found``. - -**Arguments:** - -+------+--------+-------------------------+ -| Name | Type | Description | -+======+========+=========================+ -| url | String | The URL to redirect to. | -+------+--------+-------------------------+ - -**Return value:** - -None. - -**Remarks:** - -This is essentially a convenience function that sets ``status`` to ``302`` and the ``Location`` header to the provided URL. - --------- - -.. _Response#status: - -``status`` -========== - -The HTTP status of the response (default = ``200``). - --------- - -.. _Response#headers: - -``headers`` -=========== - -The response's HTTP header fields. Prefer :ref:`Response#get() `/:ref:`Response#set() ` over accessing this object directly. - --------- - -.. _Response#body: - -``body`` -======== - -Key-value pairs of data to be submitted in the response body. - diff --git a/docs/api/server.md b/docs/api/server.md new file mode 100644 index 00000000..2aac12cf --- /dev/null +++ b/docs/api/server.md @@ -0,0 +1,216 @@ + + +## OAuth2Server +The main OAuth2 server class. + +**Kind**: global class + +* [OAuth2Server](#OAuth2Server) + * [new OAuth2Server(options)](#new_OAuth2Server_new) + * [.authenticate()](#OAuth2Server+authenticate) ⇒ Promise.<object> + * [.authorize(request, response, [options])](#OAuth2Server+authorize) ⇒ Promise.<object> + * [.token(request, response, [options])](#OAuth2Server+token) ⇒ Promise.<object> + + + +### new OAuth2Server(options) +Instantiates `OAuth2Server` using the supplied model. +**Remarks:** +- Any valid option for [authenticate](#OAuth2Server+authenticate), [authorize](#OAuth2Server+authorize) and [token](#OAuth2Server+token) can be passed to the constructor as well. +- The supplied options will be used as default for the other methods. + +**Returns**: [OAuth2Server](#OAuth2Server) - A new `OAuth2Server` instance. +**Throws**: + +- InvalidArgumentError if the model is missing + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | object | | Server options. | +| options.model | Model | | The Model; this is always required. | +| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. | +| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. | +| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. | +| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. | +| [options.authenticateHandler] | object | | The authenticate handler (see remarks section). | +| options.authenticateHandler.handle | function | | The actual handler function to get an authenticated user | +| [options.allowEmptyState] | boolean | false | Allow clients to specify an empty `state | +| [options.authorizationCodeLifetime] | number | 300 | Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) | +| [options.accessTokenLifetime] | number | 3600 | Lifetime of generated access tokens in seconds (default = 1 hour). | +| [options.refreshTokenLifetime] | number | 1209600 | Lifetime of generated refresh tokens in seconds (default = 2 weeks). | +| [options.allowExtendedTokenAttributes] | boolean | false | Allow extended attributes to be set on the returned token (see remarks section). | +| [options.requireClientAuthentication] | object \| boolean | object | Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. | +| [options.alwaysIssueNewRefreshToken] | boolean | true | Always revoke the used refresh token and issue a new one for the `refresh_token` grant. | +| [options.extendedGrantTypes] | object | object | Additional supported grant types. | + +**Example** +```js +const OAuth2Server = require('@node-oauth/oauth2-server'); +``` + + +### oAuth2Server.authenticate() ⇒ Promise.<object> +Authenticates a request. + +**Kind**: instance method of [OAuth2Server](#OAuth2Server) +**Returns**: Promise.<object> - A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. + In case of an error, the promise rejects with one of the error types derived from `OAuthError`. +**Throws**: + +- UnauthorizedRequestError The protected resource request failed authentication. + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. | +| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. | +| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. | +| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. | + +**Example** +```js +const oauth = new OAuth2Server({model: ...}); +function authenticateHandler(options) { + return function(req, res, next) { + let request = new Request(req); + let response = new Response(res); + return oauth.authenticate(request, response, options) + .then(function(token) { + res.locals.oauth = {token: token}; + next(); + }) + .catch(function(err) { + // handle error condition + }); + } +} +``` + + +### oAuth2Server.authorize(request, response, [options]) ⇒ Promise.<object> +Authorizes a token request. +**Remarks:** + +If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. + +In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. +The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. +If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. + +```js +let authenticateHandler = { + handle: function(request, response) { + return // get authenticated user; + } +}; +``` +When working with a session-based login mechanism, the handler can simply look like this: +```js +let authenticateHandler = { + handle: function(request, response) { + return request.session.user; + } +}; +``` + +**Kind**: instance method of [OAuth2Server](#OAuth2Server) +**Returns**: Promise.<object> - A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` + In case of an error, the promise rejects with one of the error types derived from `OAuthError`. +**Throws**: + +- AccessDeniedError The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| request | Request | | the Request instance object | +| [request.query.allowed] | string | | `'false'` to deny the authorization request (see remarks section). | +| response | Response | | the Response instance object | +| [options] | object | | handler options | +| [options.authenticateHandler] | object | | The authenticate handler (see remarks section). | +| options.authenticateHandler.handle | function | | The actual handler function to get an authenticated user | +| [options.allowEmptyState] | boolean | false | Allow clients to specify an empty `state | +| [options.authorizationCodeLifetime] | number | 300 | Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) | + +**Example** +```js +const oauth = new OAuth2Server({model: ...}); +function authorizeHandler(options) { + return function(req, res, next) { + let request = new Request(req); + let response = new Response(res); + return oauth.authorize(request, response, options) + .then(function(code) { + res.locals.oauth = {code: code}; + next(); + }) + .catch(function(err) { + // handle error condition + }); + } +} +``` + + +### oAuth2Server.token(request, response, [options]) ⇒ Promise.<object> +Retrieves a new token for an authorized token request. +**Remarks:** +If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. +By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. +```js +let options = { + // ... + // Allow token requests using the password grant to not include a client_secret. + requireClientAuthentication: {password: false} +}; +``` +`options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: +```js +let options = { + // ... + extendedGrantTypes: { + 'urn:foo:bar:baz': MyGrantType + } +}; +``` +For information on how to implement a handler for a custom grant type see the extension grants. + +**Kind**: instance method of [OAuth2Server](#OAuth2Server) +**Returns**: Promise.<object> - A `Promise` that resolves to the token object returned from the model's `saveToken` method. + In case of an error, the promise rejects with one of the error types derived from `OAuthError`. +**Throws**: + +- InvalidGrantError The access token request was invalid or not authorized. + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| request | Request | | the Request instance object | +| response | Response | | the Response instance object | +| [options] | object | | handler options | +| [options.accessTokenLifetime] | number | 3600 | Lifetime of generated access tokens in seconds (default = 1 hour). | +| [options.refreshTokenLifetime] | number | 1209600 | Lifetime of generated refresh tokens in seconds (default = 2 weeks). | +| [options.allowExtendedTokenAttributes] | boolean | false | Allow extended attributes to be set on the returned token (see remarks section). | +| [options.requireClientAuthentication] | object \| boolean | object | Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. | +| [options.alwaysIssueNewRefreshToken] | boolean | true | Always revoke the used refresh token and issue a new one for the `refresh_token` grant. | +| [options.extendedGrantTypes] | object | object | Additional supported grant types. | + +**Example** +```js +const oauth = new OAuth2Server({model: ...}); +function tokenHandler(options) { + return function(req, res, next) { + let request = new Request(req); + let response = new Response(res); + return oauth.token(request, response, options) + .then(function(code) { + res.locals.oauth = {token: token}; + next(); + }) + .catch(function(err) { + // handle error condition + }); + } +} +``` diff --git a/docs/api/token-types/bearer-token-type.md b/docs/api/token-types/bearer-token-type.md new file mode 100644 index 00000000..30030e6d --- /dev/null +++ b/docs/api/token-types/bearer-token-type.md @@ -0,0 +1,27 @@ + + +## BearerTokenType +**Kind**: global class + +* [BearerTokenType](#BearerTokenType) + * [new BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes)](#new_BearerTokenType_new) + * [.valueOf()](#BearerTokenType+valueOf) + + + +### new BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) + +| Param | +| --- | +| accessToken | +| accessTokenLifetime | +| refreshToken | +| scope | +| customAttributes | + + + +### bearerTokenType.valueOf() +Retrieve the value representation. + +**Kind**: instance method of [BearerTokenType](#BearerTokenType) diff --git a/docs/api/token-types/mac-token-type.md b/docs/api/token-types/mac-token-type.md new file mode 100644 index 00000000..dd87d949 --- /dev/null +++ b/docs/api/token-types/mac-token-type.md @@ -0,0 +1,4 @@ + + +## MacTokenType +**Kind**: global class diff --git a/docs/api/utils/crypto-util.md b/docs/api/utils/crypto-util.md new file mode 100644 index 00000000..19d943d3 --- /dev/null +++ b/docs/api/utils/crypto-util.md @@ -0,0 +1,19 @@ + + +## CryptoUtil + + +### CryptoUtil~createHash(algorithm, data, [encoding], output) ⇒ Buffer \| string +Creates a new hash by given algorithm, data and digest encoding. +Defaults to sha256. + +**Kind**: inner method of [CryptoUtil](#module_CryptoUtil) +**Returns**: Buffer \| string - if {output} is undefined, a {Buffer} is returned, otherwise a {String} + +| Param | Type | Description | +| --- | --- | --- | +| algorithm | string | the hash algorithm, default is 'sha256' | +| data | Buffer \| string \| TypedArray \| DataView | the data to hash | +| [encoding] | string | optional, the encoding of the input | +| output | 'base64' \| 'base64url' \| 'binary' \| 'hex' \| undefined | optional, the desired output type | + diff --git a/docs/api/utils/date-util.md b/docs/api/utils/date-util.md new file mode 100644 index 00000000..ec3e26dd --- /dev/null +++ b/docs/api/utils/date-util.md @@ -0,0 +1,16 @@ + + +## DateUtil + + +### DateUtil~getLifetimeFromExpiresAt(expiresAt) ⇒ number +Returns the remaining seconds of expiration from now. +If the value is less than or equal zero, then it is considered expired. + +**Kind**: inner method of [DateUtil](#module_DateUtil) +**Returns**: number - The number of seconds until the expiration date. + +| Param | Type | Description | +| --- | --- | --- | +| expiresAt | Date | The date at which something (e.g. a token) expires. | + diff --git a/docs/api/utils/scope-util.md b/docs/api/utils/scope-util.md new file mode 100644 index 00000000..18ea57b5 --- /dev/null +++ b/docs/api/utils/scope-util.md @@ -0,0 +1,21 @@ + + +## ScopeUtil + + +### ScopeUtil~parseScope(requestedScope) ⇒ undefined \| Array.<string> +Utility to parse and validate a scope string. +Uses `isFormat` from [https://github.com/node-oauth/formats](https://github.com/node-oauth/formats) to +validate scopes against `nqchar` format. + +**Kind**: inner method of [ScopeUtil](#module_ScopeUtil) +**Throws**: + +- InvalidScopeError if the type is not null, undefined or a string. + +**See**: {https://github.com/node-oauth/formats} + +| Param | Type | +| --- | --- | +| requestedScope | string \| undefined \| null | + diff --git a/docs/api/utils/string-util.md b/docs/api/utils/string-util.md new file mode 100644 index 00000000..e00c273f --- /dev/null +++ b/docs/api/utils/string-util.md @@ -0,0 +1,15 @@ + + +## StringUtil + + +### StringUtil~base64URLEncode(str) ⇒ string +Encodes a string to a valid base64 string that +can be used as URL component. + +**Kind**: inner method of [StringUtil](#module_StringUtil) + +| Param | +| --- | +| str | + diff --git a/docs/api/utils/token-util.md b/docs/api/utils/token-util.md new file mode 100644 index 00000000..b1c8ca04 --- /dev/null +++ b/docs/api/utils/token-util.md @@ -0,0 +1,9 @@ + + +## TokenUtil + + +### TokenUtil~generateRandomToken() ⇒ Promise.<string> +Generates random token as 32 byte hex string. + +**Kind**: inner method of [TokenUtil](#module_TokenUtil) diff --git a/docs/build-api.js b/docs/build-api.js new file mode 100644 index 00000000..d0b64178 --- /dev/null +++ b/docs/build-api.js @@ -0,0 +1,42 @@ +const { promises: fs } = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const glob = require('glob'); + +(async function () { + // Usage: + // node scripts/build-docs-cli.js [srcRoot] [outRoot] + // Examples: + // node scripts/build-docs-cli.js -> src: "src", out: "docs" + // node scripts/build-docs-cli.js lib -> src: "lib", out: "docs" + // node scripts/build-docs-cli.js lib build/docs -> src: "lib", out: "build/docs" + + const srcRoot = process.argv[2] || 'lib/'; + const outRoot = process.argv[3] || 'docs/api/'; + const pattern = `${srcRoot.replace(/\\/g, '/')}/**/*.js`; + const files = glob.sync(pattern, { nodir: true }); + + if (files.length === 0) { + console.error(`No files found for pattern: ${pattern}`); + process.exit(1); + } + + for (const src of files) { + const rel = path.relative(srcRoot, src); + const outPath = path.join(outRoot, rel).replace(/\.(js|ts)$/, '.md'); + + await fs.mkdir(path.dirname(outPath), { recursive: true }); + + try { + const md = execFileSync( + process.platform === 'win32' ? 'npx.cmd' : 'npx', + ['jsdoc-to-markdown', src], + { encoding: 'utf8' } + ); + await fs.writeFile(outPath, md, 'utf8'); + console.log('Wrote', outPath); + } catch (err) { + console.error('Failed:', src, err.message); + } + } +})(); \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 2abf9c3f..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @node-oauth/oauth2-server documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 17 16:47:05 2016. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -from sphinx.errors import SphinxError - -sys.path.append(os.path.dirname(__file__)) -import npm_conf - -# Retrieve values from package.json used throughout the configuration. -config = npm_conf.get_config() - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.3' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.todo', 'sphinx_rtd_theme'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = config['name'] -copyright = '{copyright_year}, {organization}'.format(**config) - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = config['short_version'] -# The full version, including alpha/beta/rc tags. -release = config['version'] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "sphinx_rtd_theme" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = '_static/favicon.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = '{name}doc'.format(**config) - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', '{name}.tex'.format(**config), '{name} Documentation'.format(**config), - '{docs_author} \\textless{{}}{docs_author_email}\\textgreater{{}}'.format(**config), 'manual') -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', config['name'], '{name} Documentation'.format(**config), - ['{docs_author} <{docs_author_email}>'.format(**config)], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', config['name'], '{name} Documentation'.format(**config), - '{docs_author} <{docs_author_email}>'.format(**config), - config['name'], 'OAuth2 server for Node.js.', 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} - -todo_include_todos = True - -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -if not on_rtd: - try: - import sphinx_rtd_theme - except ImportError as err: - msg = [ - ' ' + str(err), - ' If you want to build docs locally please download the Read the Docs sphinx theme first:', - ' https://github.com/rtfd/sphinx_rtd_theme' - ] - raise SphinxError('\n'.join(msg)) - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -html_theme_options = { - 'sticky_navigation': True -} - -highlight_language = 'js' - -def setup(app): - app.add_css_file('custom.css') - diff --git a/docs/docs/adapters.rst b/docs/docs/adapters.rst deleted file mode 100644 index 139995ee..00000000 --- a/docs/docs/adapters.rst +++ /dev/null @@ -1,36 +0,0 @@ -========== - Adapters -========== - -The *@node-oauth/oauth2-server* module is typically not used directly but through one of the available adapters, converting the interface to a suitable one for the HTTP server framework in use. - -.. framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_ (not maintained by us). - -- express-oauth-server_ for Express_ -- koa-oauth-server_ for Koa_ - -.. _express-oauth-server: https://www.npmjs.com/package/@node-oauth/express-oauth-server -.. _Express: https://npmjs.org/package/express -.. _koa-oauth-server: https://npmjs.org/package/koa-oauth-server -.. _Koa: https://npmjs.org/package/koa - - -Writing Adapters -================ - -Adapters typically do the following: - -- Inherit from :doc:`OAuth2Server `. - -- Override :ref:`authenticate() `, :ref:`authorize() ` and :ref:`token() `. - - Each of these functions should: - - - Create :doc:`Request ` and :doc:`Response ` objects from their framework-specific counterparts. - - - Call the original function. - - - Copy all fields from the :doc:`Response ` back to the framework-specific request object and send it. - -Adapters should preserve functionality provided by *@node-oauth/oauth2-server* but are free to add additional features that make sense for the respective HTTP server framework. - diff --git a/docs/docs/getting-started.rst b/docs/docs/getting-started.rst deleted file mode 100644 index 9d86c15b..00000000 --- a/docs/docs/getting-started.rst +++ /dev/null @@ -1,105 +0,0 @@ -================= - Getting Started -================= - -.. _installation: - -Installation -============ - -oauth2-server_ is available via npm_. - -.. _oauth2-server: https://www.npmjs.com/package/@node-oauth/oauth2-server -.. _npm: https://npmjs.org - -.. code-block:: sh - - $ npm install @node-oauth/oauth2-server - -.. note:: The *@node-oauth/oauth2-server* module is framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_. If you're using one of those frameworks it is strongly recommended to use the respective adapter module instead of rolling your own. - -.. _Express: https://www.npmjs.com/package/@node-oauth/express-oauth-server -.. _Koa: https://npmjs.org/package/koa-oauth-server - - -.. _features: - -Features -======== - -- Supports :ref:`authorization code `, :ref:`client credentials `, :ref:`refresh token ` and :ref:`password ` grant, as well as :ref:`extension grants `, with scopes. -- Can be used with *promises*, *ES6 generators* and *async*/*await*. -- Fully :rfc:`6749` and :rfc:`6750` compliant. -- Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc. -- Complete `test suite`_. - -.. _test suite: https://github.com/node-oauth/node-oauth2-server/tree/master/test - - -.. _quick-start: - -Quick Start -=========== - -:doc:`/api/oauth2-server` - -:: - - const OAuth2Server = require('@node-oauth/oauth2-server'); - - const oauth = new OAuth2Server({ - model: require('./model') - }); - -:doc:`/api/request` and :doc:`/api/response` - -:: - - const Request = OAuth2Server.Request; - const Response = OAuth2Server.Response; - - let request = new Request({/*...*/}); - let response = new Response({/*...*/}); - -:ref:`OAuth2Server#authenticate() ` - -:: - - oauth.authenticate(request, response) - .then((token) => { - // The request was successfully authenticated. - }) - .catch((err) => { - // The request failed authentication. - }); - -:ref:`OAuth2Server#authorize() ` - -:: - - const AccessDeniedError = require('@node-oauth/oauth2-server/lib/errors/access-denied-error'); - - oauth.authorize(request, response) - .then((code) => { - // The resource owner granted the access request. - }) - .catch((err) => { - if (err instanceof AccessDeniedError) { - // The resource owner denied the access request. - } else { - // Access was not granted due to some other error condition. - } - }); - -:ref:`OAuth2Server#token() ` - -:: - - oauth.token(request, response) - .then((token) => { - // The resource owner granted the access request. - }) - .catch((err) => { - // The request was invalid or not authorized. - }); - diff --git a/docs/guide/adapters.md b/docs/guide/adapters.md new file mode 100644 index 00000000..404f9bbf --- /dev/null +++ b/docs/guide/adapters.md @@ -0,0 +1,22 @@ +# Adapters + +The *@node-oauth/oauth2-server* module is typically not used directly but through one of the available adapters, converting the interface to a suitable one for the HTTP server framework in use. + +- [express-oauth-server](https://www.npmjs.com/package/@node-oauth/express-oauth-server) for [Express](https://npmjs.org/package/express) +- [koa-oauth-server](https://npmjs.org/package/koa-oauth-server) for [Koa](https://npmjs.org/package/koa) + +## Writing Adapters + +Adapters typically do the following: + +- Inherit from `OAuth2Server `. + +- Override `authenticate() `, `authorize() ` and `token() `. + + Each of these functions should: + + - Create `Request ` and `Response ` objects from their framework-specific counterparts. + - Call the original function. + - Copy all fields from the `Response ` back to the framework-specific request object and send it. + +Adapters should preserve functionality provided by *@node-oauth/oauth2-server* but are free to add additional features that make sense for the respective HTTP server framework. diff --git a/docs/guide/contributing.md b/docs/guide/contributing.md new file mode 120000 index 00000000..f939e75f --- /dev/null +++ b/docs/guide/contributing.md @@ -0,0 +1 @@ +../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 00000000..0c03fff7 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,92 @@ +# Getting started with @node-oauth/oauth2-server + +## Preface +This library implements the OAuth 2.0 standard but leaves +room to custom implementation details where the standard +is either vague or explicitly states, if a detail is up to the implementation. + +Therefore, getting familiar with the several RFCs is fundamental for +understanding the standard and for a correct implementation. + +We highly encourage you to read the following +resources, before starting to operate your own authorization server: + +- [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) +- [RFC 6750 - Bearer Tokens](https://www.rfc-editor.org/rfc/rfc6750) +- [RFC 6819 - Threat Model and Security Considerations](https://www.rfc-editor.org/rfc/rfc6819) +- [RFC 7009 - Token Revocation](https://www.rfc-editor.org/rfc/rfc7009) +- [RFC 7636 - Proof Key for Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636) +- [RFC 9700 - Best Current Practice for OAuth 2.0 Security](https://www.rfc-editor.org/rfc/rfc9700) +- [OWASP OAuth2 cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html) + +## Features + +- Supports `authorization code`, `client credentials`, + `refresh token` and `password` (deprecated) grant, as well as + `extension grants`, with scopes. +- Can be used with *promises*, *ES6 generators* and *async*/*await*. +- Fully `6749` and `6750` compliant. +- Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc. +- Complete [test suite](https://github.com/node-oauth/node-oauth2-server/tree/master/test). + +## Installation + +This module is developed for Node.js and you can install this package using +npm via + +```shell +npm install --save @node-oauth/oauth2-server +``` + +Please note, that we still provide this library in "classic" commonjs +format with a pretty low node version requirement for maximum backwards +compatibility. +[A future esm release is planned.](https://github.com/node-oauth/node-oauth2-server/discussions/279) +We also highly encourage you to use supported/maintained Node.js versions. + +## Usage + +The *@node-oauth/oauth2-server* module is framework-agnostic but there are several officially supported adapters +available for popular HTTP server frameworks such +as [Express](https://www.npmjs.com/package/@node-oauth/express-oauth-server) +and [Koa](https://npmjs.org/package/koa-oauth-server). If you're using one of those frameworks it is strongly +recommended to use the respective adapter module instead of rolling your own. + +Consider the following minimal example, which you can use as a foundation for +further development: + +```js +const OAuth2Server = require('@node-oauth/oauth2-server'); +const Request = OAuth2Server.Request; +const Response = OAuth2Server.Response; +const model = require('./model'); +const oauth = new OAuth2Server({model}); + +let request = new Request({ + method: 'GET', + query: {}, + headers: {Authorization: 'Bearer foobar'} +}); + +let response = new Response({ + headers: {} +}); + +oauth.authenticate(request, response) + .then((token) => { + // The request was successfully authenticated. + }) + .catch((err) => { + // The request failed authentication. + }); +``` + +The most crucial part in this setup is the `model`. +It acts as the bridge between the OAuth2 server library and your system. + +> As a rule of thumb, the library handles the overall OAuth2 workflows, +while you can leverage the `model` to implement storage locations (In-Memory, +DB, Caching) and client management. + +Note, that different workflows require different models. +Read the [model overview](./model.md) of what is required for the model in context of specific grant types. diff --git a/docs/guide/grant-types.md b/docs/guide/grant-types.md new file mode 100644 index 00000000..b5827f49 --- /dev/null +++ b/docs/guide/grant-types.md @@ -0,0 +1,110 @@ +# OAuth 2.0 Grant Types + +[`RFC 6749`](https://www.rfc-editor.org/rfc/rfc6749) describes a number of "grants" +for a client application to acquire an access token. +You can consider a grant similar to a "workflow" that is targeted for a specific +scenario of use cases. + +## Authorization Code Grant Type + +**Defined in:** [Section 4.1 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.1). + +**Model requirements:** [Model for Authorization Code Grant](./model#authorization-code-grant) + +An authorization code is a credential representing the resource owner's authorization +(to access its protected resources) which is used by the client to obtain an access token. + +## Refresh Token Grant Type + +**Defined in:** [Section 6 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-6). + +**Model requirements:** [Model for Refresh Code Grant](./model.md#refresh-token-grant) + +If the authorization server issued a refresh token to the client, +the client can request a refresh of their authorization token. + +## Password Grant Type + +::: warning +Password grant type is deprecated and should not be used at all. +Use this type only for legacy support and consider it inherently unsafe. +Read more in the following resources: + +- [RFC9700](https://www.ietf.org/rfc/rfc9700.html#name-resource-owner-password-cre) +- [OWASP OAuth2 cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html) + ::: + +**Defined in:** [Section 4.3 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.3). + +**Model requirements:** [Model for Password Grant](./model.md#password-grant) + +The password grant is suitable for clients capable of obtaining the resource owner's credentials ( +username and password, typically using an interactive form). + +## Client Credentials Grant Type + +**Defined in:** [Section 4.4 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.4). + +**Model requirements:** [Model for Refresh Token Code Grant](./model.md#refresh-token-grant) + +The client can request an access token using only its client credentials (or other supported means of authentication) +when requesting access to the protected resources under its control. +The client credentials grant type **must** only be used by confidential clients. + +## Extension Grants + +**Defined in:** [Section 4.5 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.4). + +Create a subclass of `AbstractGrantType` and create methods handle +and saveToken along with other required methods according to your needs: + +``` js +const OAuth2Server = require('@node-oauth/oauth2-server'); +const { + AbstractGrantType, + InvalidArgumentError, + InvalidRequestError +} = OAuth2Server; + +class MyCustomGrantType extends AbstractGrantType { + constructor(opts) { + super(opts); + } + + async handle(request, client) { + if (!request) throw new InvalidArgumentError('Missing `request`'); + if (!client) throw new InvalidArgumentError('Missing `client`'); + + let scope = this.getScope(request); + let user = await this.getUserBySomething(request); + + return this.saveToken(user, client, scope); + } + + async saveToken(user, client, scope) { + this.validateScope(user, client, scope); + + let token = { + accessToken: await this.generateAccessToken(client, user, scope), + accessTokenExpiresAt: this.getAccessTokenExpiresAt(), + refreshToken: await this.generateRefreshToken(client, user, scope), + refreshTokenExpiresAt: this.getRefreshTokenExpiresAt(), + scope: scope + }; + + return this.model.saveToken(token, client, user); + } + + async getUserBySomething(request) { + //Get user's data by corresponding data (FB User ID, Google, etc.), etc. + } +} + +module.exports = MyCustomGrantType; +``` + +Extension grants are registered +through [OAuth2Server#token](../api/server.md#oauth2servertokenrequest-response-options--codepromiseobjectcode) ( +`options.extendedGrantTypes`). + +This might require you to approve the new `grant_type` for a particular `client` if you do checks on valid grant types. diff --git a/docs/misc/migrating-to-v5.rst b/docs/guide/migrating-to-v5.md similarity index 62% rename from docs/misc/migrating-to-v5.rst rename to docs/guide/migrating-to-v5.md index 279c343b..151f806d 100644 --- a/docs/misc/migrating-to-v5.rst +++ b/docs/guide/migrating-to-v5.md @@ -1,44 +1,34 @@ -=========================== - Migrating to 5.x -=========================== +# Migrating to 5.x This guide covers the most breaking changes, in case you updated from an earlier version. -------------------- -Requires Node >= 16 -------------------- +## Requires Node \>= 16 Due to Node 14 reaching end of life (EOL; which implies no security updates) this version requires at least Node 16. -Future versions of the 5.x major releases will update to a newer Node LTS, once the current one reaches EOL. +Future versions of the 5.x major releases may update to a newer Node version, if necessary. Note, that we also won't regard any security patches to problems that are a direct consequence of using a Node version that reached EOL. ------------------------- -Removed callback support ------------------------- +## Removed callback support -With beginning of release 5.0.0 this module dropped all callback support and uses `async/await` +With beginning of release 5.0.0 this module dropped all callback support and uses async/await for all asynchronous operations. -This implies you either need to have a more recent Node.js environment that natively supports `async/await` +This implies you either need to have a more recent Node.js environment that natively supports async/await or your project uses tools to support at least Promises. ------------------ -Update your model ------------------ +## Update your model -The model functions is now expected to return a Promise (or being declared as `async function`), +The model functions is now expected to return a Promise (or being declared as async function), since callback support is dropped. Note: Synchronous model functions are still supported. However, we recommend to use Promise or async, if database operations (or other heavy operations) are part of a specific model function implementation. ------------------- -Scope is now Array ------------------- +## Scope is now Array -In earlier versions we allowed `scope` to be strings with words, separated by empty space. +In earlier versions we allowed scope to be strings with words, separated by empty space. With beginning of 5.0.0 the scope parameter needs to be an Array of strings. This implies to requests, responses and model implementations where scope is included. diff --git a/docs/guide/model.md b/docs/guide/model.md new file mode 100644 index 00000000..2566be25 --- /dev/null +++ b/docs/guide/model.md @@ -0,0 +1,96 @@ +# Model Overview + +[OAuth2Server](../api/server.md) requires a model object through which some aspects of storage, +retrieval and custom validation are abstracted. + +## Migration Notes + +**Version \>=5.x:** Callback support has been removed! Each model function supports either sync or async +(`Promise` or `async function`) return values. + +**Version \<=4.x:** Each model function supports *promises*, *Node-style callbacks*, *ES6 generators* +and *async*/*await* (using [Babel](https://babeljs.io)). Note that promise support implies support for returning plain +values where asynchronism is not required. + +## Request Authentication + +See [Section 2 of RFC 6750](https://www.rfc-editor.org/rfc/rfc6750#section-2) + +The authorization server authenticates requests that are +sent to the resource server by verifying the included bearer token. + +Model functions used during request authentication: + +- [getAccessToken](../api/model.md#modelgetaccesstokenaccesstoken--code-promiseaccesstokendata-code) +- [verifyScope](../api/model.md#modelverifyscopeaccesstoken-scope--codepromisebooleancode) + + +## Grant Types + +For each [grant type](./grant-types.md) there are different model required, optional or unused. +The following sections describe the grant types and the model functions, required for +the grants. + +### Authorization Code Grant + +Model functions required by the [authorization code grant](./grant-types.md#authorization-code-grant-type): + +- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code) +- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode) +- [getAuthorizationCode](../api/model.md#modelgetauthorizationcodeauthorizationcode--code-promiseauthorizationcodedata-code) +- [saveAuthorizationCode](../api/model.md#modelsaveauthorizationcodecode-client-user--codepromiseobjectcode) +- [revokeAuthorizationCode](../api/model.md#modelrevokeauthorizationcodecode--codepromisebooleancode) + +Required if no custom authenticate handler used: +- [verifyScope](../api/model.md#modelverifyscopeaccesstoken-scope--codepromisebooleancode) + +Required if custom authenticate handler used: +- [getAccessToken](../api/model.md#modelgetaccesstokenaccesstoken--code-promiseaccesstokendata-code) + +**Optional, but recommended:** +- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode) +- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode) +- [generateAuthorizationCode](../api/model.md#modelgenerateauthorizationcodeclient-user-scope--codepromisestringcode) +- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode) +- [validateRedirectUri](../api/model.md#modelvalidateredirecturiredirecturi-client--codepromisebooleancode) + +### Client Credentials Grant + +Model functions used by the [client credentials grant](grant-types.md#client-credentials-grant-type): + +- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code) +- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode) +- [getUserFromClient](../api/model.md#modelgetuserfromclientclient--codepromiseobjectcode) + +**Optional, but recommended:** +- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode) +- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode) + +### Refresh Token Grant + +Model functions used by the [refresh token grant](grant-types.md#refresh-token-grant-type): + +- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode) +- [getRefreshToken](../api/model.md#modelgetrefreshtokenrefreshtoken--code-promiserefreshtokendata-code) +- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code) +- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode) +- [revokeToken](../api/model.md#modelrevoketokentoken--codepromisebooleancode) + + +### Password Grant + +Model functions used by the [password grant](grant-types.md#password-grant-type): + +- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode) +- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode) +- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code) +- [getUser](../api/model.md#modelgetuserusername-password-client--codepromiseobjectnullundefinedfalse0code) +- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode) +- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode) + +### Extension Grants + +The authorization server may also implement custom grant types to issue access (and optionally refresh) tokens. + +See [extension grants](./grant-types.md#extension-grants) + diff --git a/docs/guide/pkce.md b/docs/guide/pkce.md new file mode 100644 index 00000000..3eace68c --- /dev/null +++ b/docs/guide/pkce.md @@ -0,0 +1,132 @@ +# PKCE Support + +Starting with release [4.3.0](https://github.com/node-oauth/node-oauth2-server/releases/tag/v4.3.0) this library +supports PKCE (Proof Key for Code Exchange by OAuth Public Clients) as defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636). + +The PKCE integrates only with the `authorization code `. The abstract workflow looks like +the following: + +``` ++-------------------+ +| Authz Server | ++--------+ | +---------------+ | +| |--(A)- Authorization Request ---->| | | +| | + t(code_verifier), t_m | | Authorization | | +| | | | Endpoint | | +| |<-(B)---- Authorization Code -----| | | +| | | +---------------+ | +| Client | | | +| | | +---------------+ | +| |--(C)-- Access Token Request ---->| | | +| | + code_verifier | | Token | | +| | | | Endpoint | | +| |<-(D)------ Access Token ---------| | | ++--------+ | +---------------+ | ++-------------------+ + +Figure 2: Abstract Protocol Flow +``` + +See [Section 1 of RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636#section-1.1). + +## 1. Authorization request + +
+ +> 1. The client creates and records a secret named the "code_verifier" and derives a transformed version "t(code_verifier)" (referred to as the "code_challenge"), which is sent in the OAuth 2.0 Authorization Request along with the transformation method "t_m". + +
+ +The following shows an example of how a client could generate a code_challenge and +code_challenge_method\` for the authorizazion request. + + const base64URLEncode = str => str.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + + // This is the code_verifier, which is INITIALLY KEPT SECRET on the client + // and which is later passed as request param to the token endpoint. + // DO NOT SEND this with the authorization request! + const codeVerifier = base64URLEncode(crypto.randomBytes(32)) + + // This is the hashed version of the verifier, which is sent to the authorization endpoint. + // This is named t(code_verifier) in the above workflow + // Send this with the authorization request! + const codeChallenge = base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) + + // This is the name of the code challenge method + // This is named t_m in the above workflow + // Send this with the authorization request! + const codeChallengeMethod = 'S256' + + // add these to the request that is fired from the client + +In this project the authorize endpoint calls OAuth2Server.prototype.authorize which itself uses AuthorizeHandler. +If your Request body contains code_challenge and code_challenge_method then PKCE is active. + + const server = new OAuth2Server({ model }) + + // this could be added to express or other middleware + const authorizeEndpoint = function (req, res, next) { + const request = new Request(req) + req.query.code_challenge // the codeChallenge value + req.query.code_challenge_method // 'S256' + + server.authorize(request, response, options) + .then(function (code) { + // add code to response, code should not contain + // code_challenge or code_challenge_method + }) + .catch(function (err) { + // handle error condition + }) + } + +## 2. Authorization response + +
+ +> 2. The Authorization Endpoint responds as usual but records "t(code_verifier)" and the transformation method. + +
+ +The `AuthorizeHandler.handle` saves code challenge and code challenge method automatically via `model.saveAuthorizationCode`. +Note that this calls your model with additional properties `code.codeChallenge` and `code.codeChallengeMethod`. + +## 3. Access Token Request + +
+ +> 3. The client then sends the authorization code in the Access Token Request as usual but includes the "code_verifier" secret generated at (A). + +
+ +This is usually done in your token endpoint, that uses `OAuth2Server.token`. + + const server = new OAuth2Server({ model }) + + // ...authorizeEndpoint + + // this could be added to express or other middleware + const tokenEndpoint = function (req, res, next) { + const request = new Request(req) + request.body.code_verifier // the non-hashed code verifier + server.token(request, response, options) + .then(function (code) { + // add code to response, code should contain + }) + .catch(function (err) { + // handle error condition + }) + } + +Note that your client should have kept `code_verifier` a secret until this step and now includes it as param for the token endpoint call. + +> 4. The authorization server transforms "code_verifier" and compares it to "t(code_verifier)" from (B). Access is denied if they are not equal. + +This will call `model.getAuthorizationCode` to load the code. +The loaded code has to contain `codeChallenge` and `codeChallengeMethod`. +If `model.saveAuthorizationCode` did not cover these values when saving the code then this step will deny the request. + +See `Model#saveAuthorizationCode` and `Model#getAuthorizationCode` diff --git a/docs/guide/token-types.md b/docs/guide/token-types.md new file mode 100644 index 00000000..8f0138a2 --- /dev/null +++ b/docs/guide/token-types.md @@ -0,0 +1,9 @@ +# OAuth 2.0 Token and Code Types + +The several grant types operate using different "token". + +## Authorization Code + +## Access Token + +## Refresh Token diff --git a/docs/images/Oauth_logo.png b/docs/images/Oauth_logo.png new file mode 100644 index 00000000..a21a6c3f Binary files /dev/null and b/docs/images/Oauth_logo.png differ diff --git a/docs/images/Oauth_logo.svg b/docs/images/Oauth_logo.svg new file mode 100644 index 00000000..e9d545a5 --- /dev/null +++ b/docs/images/Oauth_logo.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico new file mode 100644 index 00000000..407fa44c Binary files /dev/null and b/docs/images/favicon.ico differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..5c4d9618 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "@node-oauth/oauth2-server" + tagline: "Complete, compliant and well tested module for implementing an OAuth2 server in Node.js." + text: User and developer documentation + image: + src: /images/Oauth_logo.svg + alt: OAuth logo by Chris Messina, CC BY-SA 3.0,via Wikimedia Commons + actions: + - theme: brand + text: Getting started + link: /guide/getting-started + - theme: alt + text: API Docs +--- diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 7c1ea417..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,86 +0,0 @@ -========================== - @node-oauth/oauth2-server -========================== - -oauth2-server_ is a complete, compliant and well tested module for implementing an OAuth2 server in Node.js_. The project is `hosted on GitHub`_ and the included test suite is automatically `run on GitHub CI`_. - -.. _oauth2-server: https://www.npmjs.com/package/@node-oauth/oauth2-server -.. _Node.js: https://nodejs.org -.. _hosted on GitHub: https://github.com/node-oauth/node-oauth2-server -.. _run on GitHub CI: https://github.com/node-oauth/node-oauth2-server/actions - -:ref:`installation` - - -Example Usage -============= - -:: - - const OAuth2Server = require('@node-oauth/oauth2-server'); - const Request = OAuth2Server.Request; - const Response = OAuth2Server.Response; - - const oauth = new OAuth2Server({ - model: require('./model') - }); - - let request = new Request({ - method: 'GET', - query: {}, - headers: {Authorization: 'Bearer foobar'} - }); - - let response = new Response({ - headers: {} - }); - - oauth.authenticate(request, response) - .then((token) => { - // The request was successfully authenticated. - }) - .catch((err) => { - // The request failed authentication. - }); - -See the :doc:`/model/spec` of what is required from the model passed to :doc:`/api/oauth2-server`. - -Contents --------- - -.. toctree:: - Home - -.. toctree:: - :maxdepth: 1 - :caption: User Documentation - - docs/getting-started - docs/adapters - -.. toctree:: - :maxdepth: 1 - :caption: API - :includehidden: - - api/oauth2-server - api/request - api/response - api/errors/index - -.. toctree:: - :maxdepth: 1 - :caption: Model - - model/overview - model/spec - -.. toctree:: - :maxdepth: 1 - :caption: Miscellaneous - - misc/extension-grants - misc/pkce - misc/migrating-to-v5 - misc/migrating-v2-to-v3 - diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 7426caa0..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,190 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\oauth2-server.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\oauth2-server.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/docs/misc/extension-grants.rst b/docs/misc/extension-grants.rst deleted file mode 100644 index 4ce22bfd..00000000 --- a/docs/misc/extension-grants.rst +++ /dev/null @@ -1,52 +0,0 @@ -================== - Extension Grants -================== - -Create a subclass of ``AbstractGrantType`` and create methods `handle` and `saveToken` along with other required methods according to your needs: - -.. code-block:: js - - const OAuth2Server = require('@node-oauth/oauth2-server'); - const AbstractGrantType = OAuth2Server.AbstractGrantType; - const InvalidArgumentError = OAuth2Server.InvalidArgumentError; - const InvalidRequestError = OAuth2Server.InvalidRequestError; - - class MyCustomGrantType extends AbstractGrantType { - constructor(opts) { - super(opts); - } - - async handle(request, client) { - if (!request) throw new InvalidArgumentError('Missing `request`'); - if (!client) throw new InvalidArgumentError('Missing `client`'); - - let scope = this.getScope(request); - let user = await this.getUserBySomething(request); - - return this.saveToken(user, client, scope); - } - - async saveToken(user, client, scope) { - this.validateScope(user, client, scope); - - let token = { - accessToken: await this.generateAccessToken(client, user, scope), - accessTokenExpiresAt: this.getAccessTokenExpiresAt(), - refreshToken: await this.generateRefreshToken(client, user, scope), - refreshTokenExpiresAt: this.getRefreshTokenExpiresAt(), - scope: scope - }; - - return this.model.saveToken(token, client, user); - } - - async getUserBySomething(request) { - //Get user's data by corresponding data (FB User ID, Google, etc.), etc. - } - } - - module.exports = MyCustomGrantType; - -Extension grants are registered through :ref:`OAuth2Server#token() ` (``options.extendedGrantTypes``). - -This might require you to approve the new ``grant_type`` for a particular ``client`` if you do checks on valid grant types. diff --git a/docs/misc/migrating-v2-to-v3.rst b/docs/misc/migrating-v2-to-v3.rst deleted file mode 100644 index 8d1290c0..00000000 --- a/docs/misc/migrating-v2-to-v3.rst +++ /dev/null @@ -1,120 +0,0 @@ -=========================== - Migrating from 2.x to 3.x -=========================== - -This module is now promise-based but allows for **ES6 generators**, **async/await** (using *[babel](https://babeljs.io)* or node v7.6+), **node-style** callbacks and **promises** in your model. - ------------ -Middlewares ------------ - -The naming of the exposed middlewares has changed to match the OAuth2 _RFC_ more closely. Please refer to the table below: - -+-------------------+------------------------------------------------+ -| oauth2-server 2.x | @node-oauth/oauth2-server 3.x | -+===================+================================================+ -| authorise | authenticate | -+-------------------+------------------------------------------------+ -| authCodeGrant | authorize | -+-------------------+------------------------------------------------+ -| grant | token | -+-------------------+------------------------------------------------+ -| errorHandler | **removed** (now handled by external wrappers) | -+-------------------+------------------------------------------------+ -| lockdown | **removed** (specific to *Express* middleware) | -+-------------------+------------------------------------------------+ - --------------- -Server options --------------- - -The following server options can be set when instantiating the OAuth service: - -* `addAcceptedScopesHeader`: **default true** Add the `X-Accepted-OAuth-Scopes` header with a list of scopes that will be accepted -* `addAuthorizedScopesHeader`: **default true** Add the `X-OAuth-Scopes` header with a list of scopes that the user is authorized for -* `allowBearerTokensInQueryString`: **default false** Determine if the bearer token can be included in the query string (i.e. `?access_token=`) for validation calls -* `allowEmptyState`: **default false** If true, `state` can be empty or not passed. If false, `state` is required. -* `authorizationCodeLifetime`: **default 300** Default number of seconds that the authorization code is active for -* `accessTokenLifetime`: **default 3600** Default number of seconds that an access token is valid for -* `refreshTokenLifetime`: **default 1209600** Default number of seconds that a refresh token is valid for -* `allowExtendedTokenAttributes`: **default false** Allows additional attributes (such as `id_token`) to be included in token responses. -* `requireClientAuthentication`: **default true for all grant types** Allow ability to set client/secret authentication to `false` for a specific grant type. - -The following server options have changed behavior in v3.0.0: - - * `accessTokenLifetime` can no longer be set to `null` to indicate a non-expiring token. The recommend alternative is to set accessTokenLifetime to a high value. - -The following server options have been removed in v3.0.0: - -* `grants`: **removed** (now returned by the `getClient` method). -* `debug`: **removed** (not the responsibility of this module). -* `clientIdRegex`: **removed** (the `getClient` method can return `undefined` or throw an error). -* `passthroughErrors`: **removed** (not the responsibility of this module). -* `continueAfterResponse`: **removed** (not the responsibility of this module). - -------------------- -Model specification -------------------- - -* `generateAccessToken(client, user, scope)` is **optional** and should return a `String`. -* `generateAuthorizationCode()` is **optional** and should return a `String`. -* `generateRefreshToken(client, user, scope)` is **optional** and should return a `String`. -* `getAccessToken(token)` should return an object with: - - * `accessToken` (`String`) - * `accessTokenExpiresAt` (`Date`) - * `client` (`Object`), containing at least an `id` property that matches the supplied client - * `scope` (optional `String`) - * `user` (`Object`) - -* `getAuthCode()` was renamed to `getAuthorizationCode(code)` and should return: - - * `client` (`Object`), containing at least an `id` property that matches the supplied client - * `expiresAt` (`Date`) - * `redirectUri` (optional `String`) - * `user` (`Object`) - -* `getClient(clientId, clientSecret)` should return an object with, at minimum: - - * `redirectUris` (`Array`) - * `grants` (`Array`) - -* `getRefreshToken(token)` should return an object with: - - * `refreshToken` (`String`) - * `client` (`Object`), containing at least an `id` property that matches the supplied client - * `refreshTokenExpiresAt` (optional `Date`) - * `scope` (optional `String`) - * `user` (`Object`) - -* `getUser(username, password)` should return an object: - - * No longer requires that `id` be returned. - -* `getUserFromClient(client)` should return an object: - - * No longer requires that `id` be returned. - -* `grantTypeAllowed()` was **removed**. You can instead: - - * Return *falsy* in your `getClient()` - * Throw an error in your `getClient()` - -* `revokeAuthorizationCode(code)` is **required** and should return true -* `revokeToken(token)` is **required** and should return true -* `saveAccessToken()` was renamed to `saveToken(token, client, user)` and should return: - - * `accessToken` (`String`) - * `accessTokenExpiresAt` (`Date`) - * `client` (`Object`) - * `refreshToken` (optional `String`) - * `refreshTokenExpiresAt` (optional `Date`) - * `user` (`Object`) - -* `saveAuthCode()` was renamed to `saveAuthorizationCode(code, client, user)` and should return: - - * `authorizationCode` (`String`) - -* `validateScope(user, client, scope)` should return a `Boolean`. - -The full model specification is [also available](https://oauth2-server.readthedocs.io/en/latest/model/spec.html). diff --git a/docs/misc/pkce.rst b/docs/misc/pkce.rst deleted file mode 100644 index cb52f1e7..00000000 --- a/docs/misc/pkce.rst +++ /dev/null @@ -1,141 +0,0 @@ -================ - PKCE Support -================ - -Starting with release 4.3.0_ this library supports PKCE (Proof Key for Code Exchange by OAuth Public Clients) as -defined in :rfc:`7636`. - -.. _4.3.0: https://github.com/node-oauth/node-oauth2-server/releases/tag/v4.3.0 - -The PKCE integrates only with the :ref:`authorization code `. The abstract workflow looks like -the following: - -:: - - +-------------------+ - | Authz Server | - +--------+ | +---------------+ | - | |--(A)- Authorization Request ---->| | | - | | + t(code_verifier), t_m | | Authorization | | - | | | | Endpoint | | - | |<-(B)---- Authorization Code -----| | | - | | | +---------------+ | - | Client | | | - | | | +---------------+ | - | |--(C)-- Access Token Request ---->| | | - | | + code_verifier | | Token | | - | | | | Endpoint | | - | |<-(D)------ Access Token ---------| | | - +--------+ | +---------------+ | - +-------------------+ - - Figure 2: Abstract Protocol Flow - -See :rfc:`Section 1 of RFC 7636 <7636#section-1.1>`. - -1. Authorization request -======================== - -.. _PKCE#authorizationRequest: - - A. The client creates and records a secret named the "code_verifier" and derives a transformed version "t(code_verifier)" (referred to as the "code_challenge"), which is sent in the OAuth 2.0 Authorization Request along with the transformation method "t_m". - -The following shows an example of how a client could generate a `code_challenge`` and -``code_challenge_method`` for the authorizazion request. - -:: - - const base64URLEncode = str => str.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, '') - - // This is the code_verifier, which is INITIALLY KEPT SECRET on the client - // and which is later passed as request param to the token endpoint. - // DO NOT SEND this with the authorization request! - const codeVerifier = base64URLEncode(crypto.randomBytes(32)) - - // This is the hashed version of the verifier, which is sent to the authorization endpoint. - // This is named t(code_verifier) in the above workflow - // Send this with the authorization request! - const codeChallenge = base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) - - // This is the name of the code challenge method - // This is named t_m in the above workflow - // Send this with the authorization request! - const codeChallengeMethod = 'S256' - - // add these to the request that is fired from the client - -In this project the authorize endpoint calls OAuth2Server.prototype.authorize which itself uses AuthorizeHandler. -If your Request body contains code_challenge and code_challenge_method then PKCE is active. - -:: - - const server = new OAuth2Server({ model }) - - // this could be added to express or other middleware - const authorizeEndpoint = function (req, res, next) { - const request = new Request(req) - req.query.code_challenge // the codeChallenge value - req.query.code_challenge_method // 'S256' - - server.authorize(request, response, options) - .then(function (code) { - // add code to response, code should not contain - // code_challenge or code_challenge_method - }) - .catch(function (err) { - // handle error condition - }) - } - -2. Authorization response -========================= - -.. _PKCE#authorizationResponse: - - B. The Authorization Endpoint responds as usual but records "t(code_verifier)" and the transformation method. - -The ``AuthorizeHandler.handle`` saves code challenge and code challenge method automatically via ``model.saveAuthorizationCode``. -Note that this calls your model with additional properties ``code.codeChallenge`` and ``code.codeChallengeMethod``. - - -3. Access Token Request -======================= - -.. _PKCE#accessTokenRequest: - - C. The client then sends the authorization code in the Access Token Request as usual but includes the "code_verifier" secret generated at (A). - -This is usually done in your token endpoint, that uses ``OAuth2Server.token``. - -:: - - const server = new OAuth2Server({ model }) - - // ...authorizeEndpoint - - // this could be added to express or other middleware - const tokenEndpoint = function (req, res, next) { - const request = new Request(req) - request.body.code_verifier // the non-hashed code verifier - server.token(request, response, options) - .then(function (code) { - // add code to response, code should contain - }) - .catch(function (err) { - // handle error condition - }) - } - -Note that your client should have kept ``code_verifier`` a secret until this step and now includes it as param for the token endpoint call. - - - D. The authorization server transforms "code_verifier" and compares it to "t(code_verifier)" from (B). Access is denied if they are not equal. - -This will call ``model.getAuthorizationCode`` to load the code. -The loaded code has to contain ``codeChallenge`` and ``codeChallengeMethod``. -If ``model.saveAuthorizationCode`` did not cover these values when saving the code then this step will deny the request. - -See :ref:`Model#saveAuthorizationCode` and :ref:`Model#getAuthorizationCode` diff --git a/docs/model/overview.rst b/docs/model/overview.rst deleted file mode 100644 index 62c9ffa9..00000000 --- a/docs/model/overview.rst +++ /dev/null @@ -1,130 +0,0 @@ -================ - Model Overview -================ - -:doc:`/api/oauth2-server` requires a model object through which some aspects of storage, retrieval and custom validation are abstracted. - --------- - -.. _GrantTypes: - -Grant Types -=========== - -:rfc:`6749` describes a number of grants for a client application to acquire an access token. - -The following grant types are supported by *oauth2-server*: - --------- - -.. _AuthorizationCodeGrant: - -Authorization Code Grant ------------------------- - -See :rfc:`Section 4.1 of RFC 6749 <6749#section-4.1>`. - -An authorization code is a credential representing the resource owner's authorization (to access its protected resources) which is used by the client to obtain an access token. - -Model functions used by the authorization code grant: - -- :ref:`Model#generateAccessToken` -- :ref:`Model#generateRefreshToken` -- :ref:`Model#generateAuthorizationCode` -- :ref:`Model#getAuthorizationCode` -- :ref:`Model#getClient` -- :ref:`Model#saveToken` -- :ref:`Model#saveAuthorizationCode` -- :ref:`Model#revokeAuthorizationCode` -- :ref:`Model#validateScope` -- :ref:`Model#validateRedirectUri` - --------- - -.. _ClientCredentialsGrant: - -Client Credentials Grant ------------------------- - -See :rfc:`Section 4.4 of RFC 6749 <6749#section-4.4>`. - -The client can request an access token using only its client credentials (or other supported means of authentication) when requesting access to the protected resources under its control. - -.. note:: The client credentials grant type **must** only be used by confidential clients. - -Model functions used by the client credentials grant: - -- :ref:`Model#generateAccessToken` -- :ref:`Model#getClient` -- :ref:`Model#getUserFromClient` -- :ref:`Model#saveToken` -- :ref:`Model#validateScope` - --------- - -.. _RefreshTokenGrant: - -Refresh Token Grant -------------------- - -See :rfc:`Section 6 of RFC 6749 <6749#section-6>`. - -If the authorization server issued a refresh token to the client, the client can request a refresh of their authorization token. - -Model functions used by the refresh token grant: - -- :ref:`Model#generateRefreshToken` -- :ref:`Model#getRefreshToken` -- :ref:`Model#getClient` -- :ref:`Model#saveToken` -- :ref:`Model#revokeToken` - --------- - -.. _PasswordGrant: - -Password Grant --------------- - -See :rfc:`Section 4.3 of RFC 6749 <6749#section-4.3>`. - -The password grant is suitable for clients capable of obtaining the resource owner's credentials (username and password, typically using an interactive form). - -Model functions used by the password grant: - -- :ref:`Model#generateAccessToken` -- :ref:`Model#generateRefreshToken` -- :ref:`Model#getClient` -- :ref:`Model#getUser` -- :ref:`Model#saveToken` -- :ref:`Model#validateScope` - --------- - -.. _ExtensionGrants: - -Extension Grants ----------------- - -See :rfc:`Section 4.5 of RFC 6749 <6749#section-4.5>`. - -The authorization server may also implement custom grant types to issue access (and optionally refresh) tokens. - -See :doc:`/misc/extension-grants`. - --------- - -.. _RequestAuthentication: - -Request Authentication -====================== - -See :rfc:`Section 2 of RFC 6750 <6750#section-2>`. - -The authorization server authenticates requests sent to the resource server by verifying the included bearer token. - -Model functions used during request authentication: - -- :ref:`Model#getAccessToken` -- :ref:`Model#verifyScope` - diff --git a/docs/model/spec.rst b/docs/model/spec.rst deleted file mode 100644 index df97d6b9..00000000 --- a/docs/model/spec.rst +++ /dev/null @@ -1,997 +0,0 @@ -===================== - Model Specification -===================== - -**Version >=5.x:** Callback support has been removed! Each model function supports either sync or async (``Promise`` or ``async function``) return values. - -**Version <=4.x:** Each model function supports *promises*, *Node-style callbacks*, *ES6 generators* and *async*/*await* (using Babel_). Note that promise support implies support for returning plain values where asynchronism is not required. - -.. _Babel: https://babeljs.io - -:: - - const model = { - // We support returning promises. - getAccessToken: function() { - return new Promise('works!'); - }, - - // Or sync-style values - getAuthorizationCode: function() { - return 'works!' - }, - - // Or, using generators. - getClient: function*() { - yield somethingAsync(); - return 'works!'; - }, - - // Or, async/wait (using Babel). - getUser: async function() { - await somethingAsync(); - return 'works!'; - } - }; - - const OAuth2Server = require('@node-oauth/oauth2-server'); - let oauth = new OAuth2Server({model: model}); - -Code examples on this page use *promises*. - --------- - -.. _Model#generateAccessToken: - -``generateAccessToken(client, user, scope)`` -======================================================== - -Invoked to generate a new access token. - -This model function is **optional**. If not implemented, a default handler is used that generates access tokens consisting of 40 characters in the range of ``a..z0..9``. - -**Invoked during:** - -- ``authorization_code`` grant -- ``client_credentials`` grant -- ``refresh_token`` grant -- ``password`` grant - -**Arguments:** - -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| client | Object | The client the access token is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| user | Object | The user the access token is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| scope | String[] | The scopes associated with the access token. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -A ``String`` to be used as access token. - -:rfc:`RFC 6749 Appendix A.12 <6749#appendix-A.12>` specifies that access tokens must consist of characters inside the range ``0x20..0x7E`` (i.e. only printable US-ASCII characters). - -**Remarks:** - -``client`` is the object previously obtained through :ref:`Model#getClient() `. - -``user`` is the user object previously obtained through :ref:`Model#getAuthorizationCode() ` (``code.user``; authorization code grant), :ref:`Model#getUserFromClient() ` (client credentials grant), :ref:`Model#getRefreshToken() ` (``token.user``; refresh token grant) or :ref:`Model#getUser() ` (password grant). - --------- - -.. _Model#generateRefreshToken: - -``generateRefreshToken(client, user, scope)`` -========================================================= - -Invoked to generate a new refresh token. - -This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of ``a..z0..9``. - -**Invoked during:** - -- ``authorization_code`` grant -- ``refresh_token`` grant -- ``password`` grant - -**Arguments:** - -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| client | Object | The client the refresh token is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| user | Object | The user the refresh token is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| scope | String[] | The scopes associated with the refresh token. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -A ``String`` to be used as refresh token. - -:rfc:`RFC 6749 Appendix A.17 <6749#appendix-A.17>` specifies that refresh tokens must consist of characters inside the range ``0x20..0x7E`` (i.e. only printable US-ASCII characters). - -**Remarks:** - -``client`` is the object previously obtained through :ref:`Model#getClient() `. - -``user`` is the user object previously obtained through :ref:`Model#getAuthorizationCode() ` (``code.user``; authorization code grant), :ref:`Model#getRefreshToken() ` (``token.user``; refresh token grant) or :ref:`Model#getUser() ` (password grant). - --------- - -.. _Model#generateAuthorizationCode: - -``generateAuthorizationCode(client, user, scope)`` -========================================= - -Invoked to generate a new authorization code. - -This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of ``a..z0..9``. - -**Invoked during:** - -- ``authorization_code`` grant - -**Arguments:** - -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| client | Object | The client the authorization code is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| user | Object | The user the authorization code is generated for. | -+------------+----------+---------------------------------------------------------------------+ -| scope | String[] | The scopes associated with the authorization code. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -A ``String`` to be used as authorization code. - -:rfc:`RFC 6749 Appendix A.11 <6749#appendix-A.11>` specifies that authorization codes must consist of characters inside the range ``0x20..0x7E`` (i.e. only printable US-ASCII characters). - --------- - -.. _Model#getAccessToken: - -``getAccessToken(accessToken)`` -=========================================== - -Invoked to retrieve an existing access token previously saved through :ref:`Model#saveToken() `. - -This model function is **required** if :ref:`OAuth2Server#authenticate() ` is used. - -**Invoked during:** - -- request authentication - -**Arguments:** - -+-------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+=============+==========+=====================================================================+ -| accessToken | String | The access token to retrieve. | -+-------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the access token and associated data. - -+------------------------------+----------+--------------------------------------------------+ -| Name | Type | Description | -+==============================+==========+==================================================+ -| token | Object | The return value. | -+------------------------------+----------+--------------------------------------------------+ -| token.accessToken | String | The access token passed to ``getAccessToken()``. | -+------------------------------+----------+--------------------------------------------------+ -| token.accessTokenExpiresAt | Date | The expiry time of the access token. | -+------------------------------+----------+--------------------------------------------------+ -| [token.scope] | String[] | The authorized scope of the access token. | -+------------------------------+----------+--------------------------------------------------+ -| token.client | Object | The client associated with the access token. | -+------------------------------+----------+--------------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+------------------------------+----------+--------------------------------------------------+ -| token.user | Object | The user associated with the access token. | -+------------------------------+----------+--------------------------------------------------+ - -``token.client`` and ``token.user`` can carry additional properties that will be ignored by *oauth2-server*. - -**Remarks:** - -:: - - function getAccessToken(accessToken) { - // imaginary DB queries - return db.queryAccessToken({access_token: accessToken}) - .then(function(token) { - return Promise.all([ - token, - db.queryClient({id: token.client_id}), - db.queryUser({id: token.user_id}) - ]); - }) - .spread(function(token, client, user) { - return { - accessToken: token.access_token, - accessTokenExpiresAt: token.expires_at, - scope: token.scope, - client: client, // with 'id' property - user: user - }; - }); - } - --------- - -.. _Model#getRefreshToken: - -``getRefreshToken(refreshToken)`` -============================================= - -Invoked to retrieve an existing refresh token previously saved through :ref:`Model#saveToken() `. - -This model function is **required** if the ``refresh_token`` grant is used. - -**Invoked during:** - -- ``refresh_token`` grant - -**Arguments:** - -+--------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+==============+==========+=====================================================================+ -| refreshToken | String | The access token to retrieve. | -+--------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the refresh token and associated data. - -+-------------------------------+----------+----------------------------------------------------+ -| Name | Type | Description | -+===============================+==========+====================================================+ -| token | Object | The return value. | -+-------------------------------+----------+----------------------------------------------------+ -| token.refreshToken | String | The refresh token passed to ``getRefreshToken()``. | -+-------------------------------+----------+----------------------------------------------------+ -| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | -+-------------------------------+----------+----------------------------------------------------+ -| [token.scope] | String[] | The authorized scope of the refresh token. | -+-------------------------------+----------+----------------------------------------------------+ -| token.client | Object | The client associated with the refresh token. | -+-------------------------------+----------+----------------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+-------------------------------+----------+----------------------------------------------------+ -| token.user | Object | The user associated with the refresh token. | -+-------------------------------+----------+----------------------------------------------------+ - -``token.client`` and ``token.user`` can carry additional properties that will be ignored by *oauth2-server*. - -**Remarks:** - -:: - - function getRefreshToken(refreshToken) { - // imaginary DB queries - return db.queryRefreshToken({refresh_token: refreshToken}) - .then(function(token) { - return Promise.all([ - token, - db.queryClient({id: token.client_id}), - db.queryUser({id: token.user_id}) - ]); - }) - .spread(function(token, client, user) { - return { - refreshToken: token.refresh_token, - refreshTokenExpiresAt: token.expires_at, - scope: token.scope, - client: client, // with 'id' property - user: user - }; - }); - } - --------- - -.. _Model#getAuthorizationCode: - -``getAuthorizationCode(authorizationCode)`` -======================================================= - -Invoked to retrieve an existing authorization code previously saved through :ref:`Model#saveAuthorizationCode() `. - -This model function is **required** if the ``authorization_code`` grant is used. - -**Invoked during:** - -- ``authorization_code`` grant - -**Arguments:** - -+-------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+===================+==========+=====================================================================+ -| authorizationCode | String | The authorization code to retrieve. | -+-------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the authorization code and associated data. - -+--------------------+----------+--------------------------------------------------------------+ -| Name | Type | Description | -+====================+==========+==============================================================+ -| code | Object | The return value. | -+--------------------+----------+--------------------------------------------------------------+ -| code.code | String | The authorization code passed to ``getAuthorizationCode()``. | -+--------------------+----------+--------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+--------------------+----------+--------------------------------------------------------------+ -| [code.redirectUri] | String | The redirect URI of the authorization code. | -+--------------------+----------+--------------------------------------------------------------+ -| [code.scope] | String[] | The authorized scope of the authorization code. | -+--------------------+----------+--------------------------------------------------------------+ -| code.client | Object | The client associated with the authorization code. | -+--------------------+----------+--------------------------------------------------------------+ -| code.client.id | String | A unique string identifying the client. | -+--------------------+----------+--------------------------------------------------------------+ -| code.user | Object | The user associated with the authorization code. | -+--------------------+----------+--------------------------------------------------------------+ - -``code.client`` and ``code.user`` can carry additional properties that will be ignored by *oauth2-server*. - -**Remarks:** - -:: - - function getAuthorizationCode(authorizationCode) { - // imaginary DB queries - return db.queryAuthorizationCode({authorization_code: authorizationCode}) - .then(function(code) { - return Promise.all([ - code, - db.queryClient({id: code.client_id}), - db.queryUser({id: code.user_id}) - ]); - }) - .spread(function(code, client, user) { - return { - authorizationCode: code.authorization_code, - expiresAt: code.expires_at, - redirectUri: code.redirect_uri, - scope: code.scope, - client: client, // with 'id' property - user: user - }; - }); - } - --------- - -.. _Model#getClient: - -``getClient(clientId, clientSecret)`` -================================================= - -Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. - -This model function is **required** for all grant types. - -**Invoked during:** - -- ``authorization_code`` grant -- ``client_credentials`` grant -- ``refresh_token`` grant -- ``password`` grant - -**Arguments:** - -+--------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+==============+==========+=====================================================================+ -| clientId | String | The client id of the client to retrieve. | -+--------------+----------+---------------------------------------------------------------------+ -| clientSecret | String | The client secret of the client to retrieve. Can be ``null``. | -+--------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the client and associated data, or a falsy value if no such client could be found. - -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| Name | Type | Description | -+===============================+===============+======================================================================================+ -| client | Object | The return value. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| client.id | String | A unique string identifying the client. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| [client.redirectUris] | Array | Redirect URIs allowed for the client. Required for the ``authorization_code`` grant. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| client.grants | Array | Grant types allowed for the client. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| [client.accessTokenLifetime] | Number | Client-specific lifetime of generated access tokens in seconds. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ -| [client.refreshTokenLifetime] | Number | Client-specific lifetime of generated refresh tokens in seconds. | -+-------------------------------+---------------+--------------------------------------------------------------------------------------+ - -The return value (``client``) can carry additional properties that will be ignored by *oauth2-server*. - -**Remarks:** - -:: - - function getClient(clientId, clientSecret) { - // imaginary DB query - let params = {client_id: clientId}; - if (clientSecret) { - params.client_secret = clientSecret; - } - return db.queryClient(params) - .then(function(client) { - return { - id: client.id, - redirectUris: client.redirect_uris, - grants: client.grants - }; - }); - } - --------- - -.. _Model#getUser: - -``getUser(username, password, client)`` -=========================================== - -Invoked to retrieve a user using a username/password combination. - -This model function is **required** if the ``password`` grant is used. - -**Invoked during:** - -- ``password`` grant - -**Arguments:** - -+-------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+===================+==========+=====================================================================+ -| username | String | The username of the user to retrieve. | -+-------------------+----------+---------------------------------------------------------------------+ -| password | String | The user's password. | -+-------------------+----------+---------------------------------------------------------------------+ -| client (optional) | Client | The client. | -+-------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - -**Remarks:** - -:: - - function getUser(username, password) { - // imaginary DB query - return db.queryUser({username: username, password: password}); - } - --------- - -.. _Model#getUserFromClient: - -``getUserFromClient(client)`` -========================================= - -Invoked to retrieve the user associated with the specified client. - -This model function is **required** if the ``client_credentials`` grant is used. - -**Invoked during:** - -- ``client_credentials`` grant - -**Arguments:** - -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| client | Object | The client to retrieve the associated user for. | -+------------+----------+---------------------------------------------------------------------+ -| client.id | String | A unique string identifying the client. | -+------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - -**Remarks:** - -``client`` is the object previously obtained through :ref:`Model#getClient() `. - -:: - - function getUserFromClient(client) { - // imaginary DB query - return db.queryUser({id: client.user_id}); - } - --------- - -.. _Model#saveToken: - -``saveToken(token, client, user)`` -============================================== - -Invoked to save an access token and optionally a refresh token, depending on the grant type. - -This model function is **required** for all grant types. - -**Invoked during:** - -- ``authorization_code`` grant -- ``client_credentials`` grant -- ``refresh_token`` grant -- ``password`` grant - -**Arguments:** - -+-------------------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+===============================+==========+=====================================================================+ -| token | Object | The token(s) to be saved. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.accessToken | String | The access token to be saved. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.accessTokenExpiresAt | Date | The expiry time of the access token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| [token.refreshToken] | String | The refresh token to be saved. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | Stringp[] | The authorized scope of the token(s). | -+-------------------------------+----------+---------------------------------------------------------------------+ -| client | Object | The client associated with the token(s). | -+-------------------------------+----------+---------------------------------------------------------------------+ -| user | Object | The user associated with the token(s). | -+-------------------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -An ``Object`` representing the token(s) and associated data. - -+-----------------------------+----------+----------------------------------------------+ -| Name | Type | Description | -+=============================+==========+==============================================+ -| token | Object | The return value. | -+-----------------------------+----------+----------------------------------------------+ -| token.accessToken | String | The access token passed to ``saveToken()``. | -+-----------------------------+----------+----------------------------------------------+ -| token.accessTokenExpiresAt | Date | The expiry time of the access token. | -+-----------------------------+----------+----------------------------------------------+ -| token.refreshToken | String | The refresh token passed to ``saveToken()``. | -+-----------------------------+----------+----------------------------------------------+ -| token.refreshTokenExpiresAt | Date | The expiry time of the refresh token. | -+-----------------------------+----------+----------------------------------------------+ -| [token.scope] | String[] | The authorized scope of the access token. | -+-----------------------------+----------+----------------------------------------------+ -| token.client | Object | The client associated with the access token. | -+-----------------------------+----------+----------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+-----------------------------+----------+----------------------------------------------+ -| token.user | Object | The user associated with the access token. | -+-----------------------------+----------+----------------------------------------------+ - -``token.client`` and ``token.user`` can carry additional properties that will be ignored by *oauth2-server*. - -If the ``allowExtendedTokenAttributes`` server option is enabled (see :ref:`OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. - -**Remarks:** - -:: - - function saveToken(token, client, user) { - // imaginary DB queries - let fns = [ - db.saveAccessToken({ - access_token: token.accessToken, - expires_at: token.accessTokenExpiresAt, - scope: token.scope, - client_id: client.id, - user_id: user.id - }), - db.saveRefreshToken({ - refresh_token: token.refreshToken, - expires_at: token.refreshTokenExpiresAt, - scope: token.scope, - client_id: client.id, - user_id: user.id - }) - ]; - return Promise.all(fns); - .spread(function(accessToken, refreshToken) { - return { - accessToken: accessToken.access_token, - accessTokenExpiresAt: accessToken.expires_at, - refreshToken: refreshToken.refresh_token, - refreshTokenExpiresAt: refreshToken.expires_at, - scope: accessToken.scope, - client: {id: accessToken.client_id}, - user: {id: accessToken.user_id} - }; - }); - } - --------- - -.. _Model#saveAuthorizationCode: - -``saveAuthorizationCode(code, client, user)`` -========================================================= - -Invoked to save an authorization code. - -This model function is **required** if the ``authorization_code`` grant is used. - -**Invoked during:** - -- ``authorization_code`` grant - -**Arguments:** - -+------------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+========================+==========+=====================================================================+ -| code | Object | The code to be saved. | -+------------------------+----------+---------------------------------------------------------------------+ -| code.authorizationCode | String | The authorization code to be saved. | -+------------------------+----------+---------------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+------------------------+----------+---------------------------------------------------------------------+ -| code.redirectUri | String | The redirect URI associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------------+ -| [code.scope] | String[] | The authorized scope of the authorization code. | -+------------------------+----------+---------------------------------------------------------------------+ -| client | Object | The client associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------------+ -| user | Object | The user associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------------+ - -.. todo:: Is ``code.scope`` really optional? - -**Return value:** - -An ``Object`` representing the authorization code and associated data. - -+------------------------+----------+---------------------------------------------------------------+ -| Name | Type | Description | -+========================+==========+===============================================================+ -| code | Object | The return value. | -+------------------------+----------+---------------------------------------------------------------+ -| code.authorizationCode | String | The authorization code passed to ``saveAuthorizationCode()``. | -+------------------------+----------+---------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+------------------------+----------+---------------------------------------------------------------+ -| code.redirectUri | String | The redirect URI associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------+ -| [code.scope] | String[] | The authorized scope of the authorization code. | -+------------------------+----------+---------------------------------------------------------------+ -| code.client | Object | The client associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------+ -| code.client.id | String | A unique string identifying the client. | -+------------------------+----------+---------------------------------------------------------------+ -| code.user | Object | The user associated with the authorization code. | -+------------------------+----------+---------------------------------------------------------------+ - -``code.client`` and ``code.user`` can carry additional properties that will be ignored by *oauth2-server*. - -**Remarks:** - -:: - - function saveAuthorizationCode(code, client, user) { - // imaginary DB queries - let authCode = { - authorization_code: code.authorizationCode, - expires_at: code.expiresAt, - redirect_uri: code.redirectUri, - scope: code.scope, - client_id: client.id, - user_id: user.id - }; - return db.saveAuthorizationCode(authCode) - .then(function(authorizationCode) { - return { - authorizationCode: authorizationCode.authorization_code, - expiresAt: authorizationCode.expires_at, - redirectUri: authorizationCode.redirect_uri, - scope: authorizationCode.scope, - client: {id: authorizationCode.client_id}, - user: {id: authorizationCode.user_id} - }; - }); - } - --------- - -.. _Model#revokeToken: - -``revokeToken(token)`` -================================== - -Invoked to revoke a refresh token. - -This model function is **required** if the ``refresh_token`` grant is used. - -**Invoked during:** - -- ``refresh_token`` grant - -**Arguments:** - -+-------------------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+===============================+==========+=====================================================================+ -| token | Object | The token to be revoked. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.refreshToken | String | The refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | String[] | The authorized scope of the refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.client | Object | The client associated with the refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+-------------------------------+----------+---------------------------------------------------------------------+ -| token.user | Object | The user associated with the refresh token. | -+-------------------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -Return ``true`` if the revocation was successful or ``false`` if the refresh token could not be found. - -**Remarks:** - -``token`` is the refresh token object previously obtained through :ref:`Model#getRefreshToken() `. - -:: - - function revokeToken(token) { - // imaginary DB queries - return db.deleteRefreshToken({refresh_token: token.refreshToken}) - .then(function(refreshToken) { - return !!refreshToken; - }); - } - --------- - -.. _Model#revokeAuthorizationCode: - -``revokeAuthorizationCode(code)`` -============================================= - -Invoked to revoke an authorization code. - -This model function is **required** if the ``authorization_code`` grant is used. - -**Invoked during:** - -- ``authorization_code`` grant - -**Arguments:** - -+--------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+====================+==========+=====================================================================+ -| code | Object | The return value. | -+--------------------+----------+---------------------------------------------------------------------+ -| code.code | String | The authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ -| [code.redirectUri] | String | The redirect URI of the authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ -| [code.scope] | String[] | The authorized scope of the authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ -| code.client | Object | The client associated with the authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ -| code.client.id | String | A unique string identifying the client. | -+--------------------+----------+---------------------------------------------------------------------+ -| code.user | Object | The user associated with the authorization code. | -+--------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -Return ``true`` if the revocation was successful or ``false`` if the authorization code could not be found. - -**Remarks:** - -``code`` is the authorization code object previously obtained through :ref:`Model#getAuthorizationCode() `. - -:: - - function revokeAuthorizationCode(code) { - // imaginary DB queries - return db.deleteAuthorizationCode({authorization_code: code.authorizationCode}) - .then(function(authorizationCode) { - return !!authorizationCode; - }); - } - --------- - -.. _Model#validateScope: - -``validateScope(user, client, scope)`` -================================================== - -Invoked to check if the requested ``scope`` is valid for a particular ``client``/``user`` combination. - -This model function is **optional**. If not implemented, any scope is accepted. - -**Invoked during:** - -- ``authorization_code`` grant -- ``client_credentials`` grant -- ``password`` grant - -**Arguments:** - -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| user | Object | The associated user. | -+------------+----------+---------------------------------------------------------------------+ -| client | Object | The associated client. | -+------------+----------+---------------------------------------------------------------------+ -| client.id | Object | A unique string identifying the client. | -+------------+----------+---------------------------------------------------------------------+ -| scope | String[] | The scopes to validate. | -+------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -Validated scopes to be used or a falsy value to reject the requested scopes. - -**Remarks:** - -``user`` is the user object previously obtained through :ref:`Model#getAuthorizationCode() ` (``code.user``; authorization code grant), :ref:`Model#getUserFromClient() ` (client credentials grant) or :ref:`Model#getUser() ` (password grant). - -``client`` is the object previously obtained through :ref:`Model#getClient ` (all grants). - -You can decide yourself whether you want to reject or accept partially valid scopes by simply filtering out invalid scopes and returning only the valid ones. - -To reject invalid or only partially valid scopes: - -:: - - // list of valid scopes - const VALID_SCOPES = ['read', 'write']; - - function validateScope(user, client, scope) { - if (!scope.every(s => VALID_SCOPES.indexOf(s) >= 0)) { - return false; - } - return scope; - } - -To accept partially valid scopes: - -:: - - // list of valid scopes - const VALID_SCOPES = ['read', 'write']; - - function validateScope(user, client, scope) { - return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); - } - --------- - -.. _Model#verifyScope: - -``verifyScope(accessToken, scope)`` -=============================================== - -Invoked during request authentication to check if the provided access token was authorized the requested scopes. - -This model function is **required** if scopes are used with :ref:`OAuth2Server#authenticate() ` -but it's never called, if you provide your own ``authenticateHandler`` to the options. - -**Invoked during:** - -- request authentication - -**Arguments:** - -+------------------------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+==============================+==========+=====================================================================+ -| token | Object | The access token to test against | -+------------------------------+----------+---------------------------------------------------------------------+ -| token.accessToken | String | The access token. | -+------------------------------+----------+---------------------------------------------------------------------+ -| [token.accessTokenExpiresAt] | Date | The expiry time of the access token. | -+------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | String[] | The authorized scope of the access token. | -+------------------------------+----------+---------------------------------------------------------------------+ -| token.client | Object | The client associated with the access token. | -+------------------------------+----------+---------------------------------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+------------------------------+----------+---------------------------------------------------------------------+ -| token.user | Object | The user associated with the access token. | -+------------------------------+----------+---------------------------------------------------------------------+ -| scope | String[] | The required scopes. | -+------------------------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -Returns ``true`` if the access token passes, ``false`` otherwise. - -**Remarks:** - -``token`` is the access token object previously obtained through :ref:`Model#getAccessToken() `. - -``scope`` is the required scope as given to :ref:`OAuth2Server#authenticate() ` as ``options.scope``. - -:: - - function verifyScope(token, requestedScopes) { - if (!token.scope) { - return false; - } - let authorizedScopes = token.scope; - return requestedScopes.every(s => authorizedScopes.includes(s)); - } - --------- - -.. _Model#validateRedirectUri: - -``validateRedirectUri(redirectUri, client)`` -================================================================ - -Invoked to check if the provided ``redirectUri`` is valid for a particular ``client``. - -This model function is **optional**. If not implemented, the ``redirectUri`` should be included in the provided ``redirectUris`` of the client. - -**Invoked during:** - -- ``authorization_code`` grant - -**Arguments:** - -+-----------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+=================+==========+=====================================================================+ -| redirect_uri | String | The redirect URI to validate. | -+-----------------+----------+---------------------------------------------------------------------+ -| client | Object | The associated client. | -+-----------------+----------+---------------------------------------------------------------------+ - -**Return value:** - -Returns ``true`` if the ``redirectUri`` is valid, ``false`` otherwise. - -**Remarks:** -When implementing this method you should take care of possible security risks related to ``redirectUri``. -.. _rfc6819: https://datatracker.ietf.org/doc/html/rfc6819 - -Section-5.2.3.5 is implemented by default. -.. _Section-5.2.3.5: https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.3.5 - -:: - - function validateRedirectUri(redirectUri, client) { - return client.redirectUris.includes(redirectUri); - } diff --git a/docs/npm_conf.py b/docs/npm_conf.py deleted file mode 100644 index 41b03819..00000000 --- a/docs/npm_conf.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -""" Helper module for sphinx' conf.py. - - Useful when building documentation for npm packages. -""" - -import json -import re -from datetime import datetime - -def load_package_json(path): - """ Loads package.json from 'path'. - """ - with open(path) as f: - return json.load(f) - -def get_short_version(version): - """ Extracts the short version ("x.y.z") from 'version'. - """ - match = re.match('^(\d+(?:\.\d+){2})', version) - if not match: - raise Error('invalid version') - return match.group() - -def get_copyright_year(base_year): - """ Returns the year(s) to be shown in the copyright notice. - - If base_year is the current year: - 'nnnn' where nnnn is 'base_year' - If the current year is larger than base_year: - 'nnnn-mmmm' where nnnn is 'base_year' and mmmm is the current year - """ - this_year = datetime.now().year - fmt = '{base_year}-{this_year}' if this_year > base_year else '{this_year}' - return fmt.format(base_year=base_year, this_year=this_year) - -def get_config(): - package = load_package_json('../package.json') - return { - 'name': package['name'], - 'version': package['version'], - 'short_version': get_short_version(package['version']), - 'organization': '@node-oauth', - 'copyright_year': get_copyright_year(2016), - # TODO: Get authors from package. - 'docs_author': 'Node-OAuth Authors', - 'docs_author_email': '' - } - diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index b61661a3..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Defining the exact version will make sure things don't break -sphinx==5.3.0 -sphinx_rtd_theme==1.1.1 -readthedocs-sphinx-search==0.3.2 \ No newline at end of file diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index 614235c7..e207f052 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -1,20 +1,22 @@ 'use strict'; -/** +/* * Module dependencies. */ const OAuthError = require('./oauth-error'); /** - * Constructor. - * - * "The resource owner or authorization server denied the request" - * + * @class + * @classDesc "The resource owner or authorization server denied the request" * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 */ - class AccessDeniedError extends OAuthError { + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ constructor(message, properties) { properties = { code: 400, @@ -26,8 +28,4 @@ class AccessDeniedError extends OAuthError { } } -/** - * Export constructor. - */ - module.exports = AccessDeniedError; diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js index 3125c75e..e0fb876c 100644 --- a/lib/errors/insufficient-scope-error.js +++ b/lib/errors/insufficient-scope-error.js @@ -1,20 +1,23 @@ 'use strict'; -/** +/* * Module dependencies. */ const OAuthError = require('./oauth-error'); /** - * Constructor. - * - * "The request requires higher privileges than provided by the access token.." - * + * @class + * @classDesc "The request requires higher privileges than provided by the access token.." * @see https://tools.ietf.org/html/rfc6750.html#section-3.1 */ class InsufficientScopeError extends OAuthError { + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ constructor(message, properties) { properties = { code: 403, @@ -26,8 +29,4 @@ class InsufficientScopeError extends OAuthError { } } -/** - * Export constructor. - */ - module.exports = InsufficientScopeError; diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index 9c9cfc52..46ba642d 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -1,16 +1,22 @@ 'use strict'; -/** +/* * Module dependencies. */ const OAuthError = require('./oauth-error'); /** - * Constructor. + * @class + * @classDesc "An argument to a function or constructor is missing or of wrong type" */ class InvalidArgumentError extends OAuthError { + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ constructor(message, properties) { properties = { code: 500, @@ -22,8 +28,4 @@ class InvalidArgumentError extends OAuthError { } } -/** - * Export constructor. - */ - module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index a1874d82..33b15035 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -1,21 +1,24 @@ 'use strict'; -/** +/* * Module dependencies. */ const OAuthError = require('./oauth-error'); /** - * Constructor. - * - * "Client authentication failed (e.g., unknown client, no client - * authentication included, or unsupported authentication method)" - * + * @class + * @classDesc "Client authentication failed (e.g., unknown client, no client + * authentication included, or unsupported authentication method)" * @see https://tools.ietf.org/html/rfc6749#section-5.2 */ class InvalidClientError extends OAuthError { + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ constructor(message, properties) { properties = { code: 400, @@ -27,8 +30,4 @@ class InvalidClientError extends OAuthError { } } -/** - * Export constructor. - */ - module.exports = InvalidClientError; diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index 4beade93..b91841f7 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -1,22 +1,25 @@ 'use strict'; -/** +/* * Module dependencies. */ const OAuthError = require('./oauth-error'); /** - * Constructor. - * - * "The provided authorization grant (e.g., authorization code, resource owner credentials) + * @class + * @classDesc "The provided authorization grant (e.g., authorization code, resource owner credentials) * or refresh token is invalid, expired, revoked, does not match the redirection URI used * in the authorization request, or was issued to another client." - * * @see https://tools.ietf.org/html/rfc6749#section-5.2 */ class InvalidGrantError extends OAuthError { + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ constructor(message, properties) { properties = { code: 400, @@ -28,8 +31,4 @@ class InvalidGrantError extends OAuthError { } } -/** - * Export constructor. - */ - module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index 0b861019..6692616e 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index fec5d826..83098dcb 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index 481717be..a3c0ff27 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index fff9660f..1291d913 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -1,14 +1,20 @@ 'use strict'; -/** +/* * Module dependencies. */ const http = require('http'); + /** - * Constructor. + * Baseclass for OAuth related Error types. + * @class */ - class OAuthError extends Error { + /** + * @constructor + * @param messageOrError + * @param properties + */ constructor(messageOrError, properties) { super(messageOrError, properties); diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index 2b9fc8c7..04202097 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -2,19 +2,23 @@ /** * Module dependencies. + * @private */ const OAuthError = require('./oauth-error'); /** - * Constructor. - * * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." - * + * @class * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 */ class ServerError extends OAuthError { + /** + * @constructor + * @param message + * @param properties + */ constructor(message, properties) { properties = { code: 503, diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index cf29c7c9..243c1acc 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js index c861eab1..a3f5ec63 100644 --- a/lib/errors/unauthorized-request-error.js +++ b/lib/errors/unauthorized-request-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index ac7a46c2..d15fd64c 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js index c480e50e..c4b68fb5 100644 --- a/lib/errors/unsupported-response-type-error.js +++ b/lib/errors/unsupported-response-type-error.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index bce24ed8..bfaa8d92 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ @@ -9,7 +9,21 @@ const InvalidScopeError = require('../errors/invalid-scope-error'); const tokenUtil = require('../utils/token-util'); const { parseScope } = require('../utils/scope-util'); +/** + * @class + * @classDesc# + */ class AbstractGrantType { + /** + * @constructor + * @param options {object} + * @param options.accessTokenLifetime {number} access token lifetime in seconds + * @param options.model {Model} the model + * @param options.refreshTokenLifetime {number} + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @throws {InvalidArgumentError} if {options.accessTokenLifeTime} is missing + * @throws {InvalidArgumentError} if {options.model} is missing + */ constructor (options) { options = options || {}; @@ -28,8 +42,16 @@ class AbstractGrantType { } /** - * Generate access token. - */ + * Generate access token. + * If the model implements `generateAccessToken` then + * this implementation will be used. + * Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. + * + * @param client + * @param user + * @param scope + * @return {Promise} + */ async generateAccessToken (client, user, scope) { if (this.model.generateAccessToken) { // We should not fall back to a random accessToken, if the model did not @@ -63,7 +85,8 @@ class AbstractGrantType { /** - * Get refresh token expiration date. + * Get refresh token expiration date (now + refresh token lifetime) + * @returns {Date} */ getRefreshTokenExpiresAt () { return new Date(Date.now() + this.refreshTokenLifetime * 1000); @@ -71,6 +94,8 @@ class AbstractGrantType { /** * Get scope from the request body. + * @param request {Request} + * @returns {string|undefined} */ getScope (request) { return parseScope(request.body.scope); @@ -78,6 +103,14 @@ class AbstractGrantType { /** * Validate requested scope. + * Delegates validation to the Model's `validateScope` method, + * if the model implements this method. + * Otherwise, treats given scope as valid. + * @param user {object} + * @param client {ClientData} + * @param scope {string} + * @return {string} the validated scope + * @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value */ async validateScope (user, client, scope) { if (this.model.validateScope) { @@ -94,8 +127,4 @@ class AbstractGrantType { } } -/** - * Export constructor. - */ - module.exports = AbstractGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 766b947c..227a6867 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ @@ -13,10 +13,14 @@ const isFormat = require('@node-oauth/formats'); const pkce = require('../pkce/pkce'); /** - * Constructor. + * @class + * @classDesc */ - class AuthorizationCodeGrantType extends AbstractGrantType { + /** + * @constructor + * @param options + */ constructor(options = {}) { if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); @@ -40,6 +44,8 @@ class AuthorizationCodeGrantType extends AbstractGrantType { /** * Handle authorization code grant. * + * @param request {Request} + * @param client {ClientData} * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ @@ -60,8 +66,11 @@ class AuthorizationCodeGrantType extends AbstractGrantType { } /** - * Get the authorization code. - */ + * Get the authorization code. + * @param request {Request} + * @param client {ClientData} + * @return {Promise<{user}>} + */ async getAuthorizationCode(request, client) { if (!request.body.code) { @@ -142,7 +151,8 @@ class AuthorizationCodeGrantType extends AbstractGrantType { * present if the redirect_uri parameter was included in the initial * authorization request as described in Section 4.1.1, and if included * ensure that their values are identical." - * + * @param request {Request} + * @param code {AuthorizationCodeData} * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ @@ -168,7 +178,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { * "The authorization code MUST expire shortly after it is issued to mitigate * the risk of leaks. [...] If an authorization code is used more than once, * the authorization server MUST deny the request." - * + * @param code {AuthorizationCodeData} * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 */ @@ -185,6 +195,12 @@ class AuthorizationCodeGrantType extends AbstractGrantType { /** * Save token. + * + * @param user {object} + * @param client {ClientData} + * @param authorizationCode {string} + * @param requestedScope {string} + * */ async saveToken(user, client, authorizationCode, requestedScope) { diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index fa5cd27a..5e6e74f6 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index b09e4993..bcb32c3a 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ @@ -12,6 +12,7 @@ const isFormat = require('@node-oauth/formats'); /** * Constructor. + * @deprecated */ class PasswordGrantType extends AbstractGrantType { diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 45237dbc..1f62f7ec 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 9faaf0d5..54674334 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ @@ -16,10 +16,20 @@ const UnauthorizedRequestError = require('../errors/unauthorized-request-error') const { parseScope } = require('../utils/scope-util'); /** - * Constructor. + * @class + * @classDesc */ - class AuthenticateHandler { + /** + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {InvalidArgumentError} if {model} is missing or does not implement `getAccessToken` + */ constructor (options) { options = options || {}; @@ -51,9 +61,11 @@ class AuthenticateHandler { } /** - * Authenticate Handler. - */ - + * Handles the authentication + * @param request {Request} + * @param response {Response} + * @return {Promise<*>} + */ async handle (request, response) { if (!(request instanceof Request)) { throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); @@ -105,9 +117,9 @@ class AuthenticateHandler { * * "Clients MUST NOT use more than one method to transmit the token in each request." * - * @see https://tools.ietf.org/html/rfc6750#section-2 + * @param {Request} + * @see {https://tools.ietf.org/html/rfc6750#section-2} */ - getTokenFromRequest (request) { const headerToken = request.get('Authorization'); const queryToken = request.query.access_token; @@ -135,7 +147,8 @@ class AuthenticateHandler { /** * Get the token from the request header. * - * @see http://tools.ietf.org/html/rfc6750#section-2.1 + * @param request {Request} + * @see {http://tools.ietf.org/html/rfc6750#section-2.1} */ getTokenFromRequestHeader (request) { @@ -160,6 +173,7 @@ class AuthenticateHandler { * are passed in page URLs, attackers might be able to steal them from the history data, * logs, or other unsecured locations." * + * @param request {Request} * @see http://tools.ietf.org/html/rfc6750#section-2.3 */ @@ -176,7 +190,7 @@ class AuthenticateHandler { * * "The HTTP request method is one for which the request-body has defined semantics. * In particular, this means that the "GET" method MUST NOT be used." - * + * @param request {Request} * @see http://tools.ietf.org/html/rfc6750#section-2.2 */ @@ -194,6 +208,7 @@ class AuthenticateHandler { /** * Get the access token from the model. + * @param token */ async getAccessToken (token) { @@ -256,8 +271,5 @@ class AuthenticateHandler { } } } -/** - * Export constructor. - */ module.exports = AuthenticateHandler; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index c44bcf8a..17bb3341 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 33c70ec1..5bcd321c 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -1,6 +1,6 @@ 'use strict'; -/** +/* * Module dependencies. */ diff --git a/lib/model.js b/lib/model.js new file mode 100644 index 00000000..2e3a5eba --- /dev/null +++ b/lib/model.js @@ -0,0 +1,622 @@ +'use strict'; + +/* + * Module dependencies + */ +const ServerError = require('./errors/server-error'); + +/** + * @typedef AccessTokenData + * @description An `Object` representing the access token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*. + * @property accessToken {string} The access token passed to `getAccessToken()` + * @property accessTokenExpiresAt {Date} The expiry time of the access token. + * @property scope {string[]} The authorized scope of the access token. + * @property client {object} The client associated with the access token. + * @property client.id {string} A unique string identifying the client. + * @property user {object} The user associated with the access token. + */ + +/** + * @typedef RefreshTokenData + * @description An `Object` representing the refresh token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*. + * @property refreshToken {string} The refresh token passed to `getRefreshToken()` + * @property refreshTokenExpiresAt {Date} The expiry time of the refresh token. + * @property scope {string[]} The authorized scope of the refresh token. + * @property client {ClientData} The client associated with the refresh token. + * @property user {object} The user associated with the access token. + */ + +/** + * @typedef AuthorizationCodeData + * @description An `Object` representing the authorization code and associated data. `code.client` and `code.user` can carry additional properties that will be ignored by *oauth2-server*. + * @property code {string} The authorization code passed to `getAuthorizationCode()`. + * @property expiresAt {Date} The expiry time of the authorization code. + * @property redirectUri {string} The redirect URI of the authorization code. + * @property scope {string[]} The authorized scope of the authorization code. + * @property client {ClientData} The client associated with the authorization code. + * @property user {object} The user associated with the access token. + */ + +/** + * @typedef ClientData + * @alias ClientData + * @description An `Object` representing the client and associated data. + * @property id {string} The authorization code passed to `getAuthorizationCode()`. + * @property redirectUris {string[]} Redirect URIs allowed for the client. Required for the `authorization_code` grant. + * @property grants {string[]} Grant types allowed for the client. + * @property accessTokenLifetime {number} Client-specific lifetime of generated access tokens in seconds. + * @property refreshTokenLifetime {number} Client-specific lifetime of generated refresh tokens in seconds. + */ + +/** + * @class + * @classdesc The Model implements the interface through + * which some aspects of storage, retrieval and custom + * validation are abstracted. + * + * Each model function is resolved async by default. + * This implies that async and sync model functions, + * as well as generators, are supported. + * + * @example + * const model = Model.from({ + * getClient: () => { ... } + * }) + */ +class Model { // eslint-disable-line no-unused-vars + /** + * Factory function to create a model form your implementation. + * @static + * @param impl {object} an object containing your model function implementations + * @return {Model} the model instance. + */ + static from (impl) { + const m = new Model(); + const nullFns = {}; + Object + .getOwnPropertyNames(Model.prototype) + .forEach((key) => { + nullFns[key] = null; + }); + Object.assign(m, nullFns, impl); + return m; + } + + /*------------------------------------------------------------------------- + | ALWAYS REQUIRED + *------------------------------------------------------------------------- + | The following functions are required by every workflow / grant type. + */ + + /** + * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. + * This model function is **required** for all grant types. + * **Invoked during:** + * + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * + * @async + * @param clientId {string} The client id of the client to retrieve. + * @param clientSecret {string?} The client secret of the client to retrieve. Can be `null`. + * @returns {Promise.} + * @fulfil {ClientData} - An `Object` representing the client and associated data, or a falsy value if no such client could be found. + * @reject {Error} - An Error type + */ + async getClient(clientId, clientSecret) { + throw new ServerError('getClient not implemented'); + } + + /** + * Invoked to save an access token and optionally a refresh token, depending on the grant type. + * This model function is **required** for all grant types. + * + * **Invoked during:** + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. + * + * @async + * @instance + * @param token {object} The token(s) to be saved. + * @param token.accessToken {string} The access token to be saved. + * @param token.accessTokenExpiresAt {Date} The expiry time of the access token. + * @param token.refreshToken {string} The refresh token to be saved. + * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token. + * @param token.scope {string[]} The authorized scope of the token(s) + * @param client {ClientData} The client associated with the token(s). + * @param user {object} The user associated with the token(s). + * @return {Promise} + * @fulfil {{accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object}} An `Object` representing the token(s) and associated data. + * @example + * function saveToken(token, client, user) { + * // imaginary DB queries + * let fns = [ + * db.saveAccessToken({ + * access_token: token.accessToken, + * expires_at: token.accessTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }), + * db.saveRefreshToken({ + * refresh_token: token.refreshToken, + * expires_at: token.refreshTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }) + * ]; + * return Promise.all(fns); + * .spread(function(accessToken, refreshToken) { + * return { + * accessToken: accessToken.access_token, + * accessTokenExpiresAt: accessToken.expires_at, + * refreshToken: refreshToken.refresh_token, + * refreshTokenExpiresAt: refreshToken.expires_at, + * scope: accessToken.scope, + * client: {id: accessToken.client_id}, + * user: {id: accessToken.user_id} + * }; + * }); + * } + */ + async saveToken(token, client, user) { + throw new ServerError('saveToken not implemented'); + } + + /*------------------------------------------------------------------------- + | PARTIALLY REQUIRED + *------------------------------------------------------------------------- + | The following functions are required by specific grant types or under + | specific conditions. + */ + + /** + * Invoked to retrieve a user using a username/password combination. + * This model function is **required** if the `password` grant is used. + * Please note, that password grant is considered unsafe. + * It is still supported but marked deprecated. + * + * **Invoked during:** + * - `password` grant + * + * @deprecated + * @async + * @param username {string} The username of the user to retrieve. + * @param password {string} The user's password. + * @param client {ClientData=} The client. + * @return {Promise} An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUser(username, password) { + * // imaginary DB query + * return db.queryUser({username: username, password: password}); + * } + */ + async getUser(username, password, client) { + throw new ServerError('getUser not implemented'); + } + + /** + * Invoked to retrieve the user associated with the specified client. + * This model function is **required** if the `client_credentials` grant is used. + * + * **Invoked during:** + * - `client_credentials` grant + * + * **Remarks:** + * + * `client` is the object previously obtained through `Model#getClient() `. + * + * @async + * @instance + * @param client {ClientData} The client to retrieve the associated user for. + * @return {Promise} An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUserFromClient(client) { + * // imaginary DB query + * return db.queryUser({id: client.user_id}); + * } + */ + async getUserFromClient(client) { + throw new ServerError('getUserFromClient not implemented'); + } + + /** + * Invoked to retrieve an existing access token, including associated data, that has previously been saved through `Model#saveToken() `. + * This model function is **required** if `OAuth2Server#authenticate() ` is used. + * + * **Invoked during:** + * - request authentication + * + * @async + + * @instance + * @param accessToken {string} The access token to retrieve. + * @return {Promise} the object, containing the data, stored with the access token + * @example + * function getAccessToken(accessToken) { + * // imaginary DB queries + * return db.queryAccessToken({access_token: accessToken}) + * .then(function(token) { + * return Promise.all([ + * token, + * db.queryClient({id: token.client_id}), + * db.queryUser({id: token.user_id}) + * ]); + * }) + * .spread(function(token, client, user) { + * return { + * accessToken: token.access_token, + * accessTokenExpiresAt: token.expires_at, + * scope: token.scope, + * client: client, // with 'id' property + * user: user + * }; + * }); + * } + */ + async getAccessToken(accessToken) { + throw new ServerError('getAccessToken not implemented'); + } + + /** + * Invoked to retrieve an existing refresh token previously saved through `Model#saveToken() `. + * This model function is **required** if the `refresh_token` grant is used. + * **Invoked during:** + * - `refresh_token` grant + * + * @async + + * @instance + * @param refreshToken {string} The access token to retrieve. + * @return {Promise} An `Object` representing the refresh token and associated data. + * @example + * function getRefreshToken(refreshToken) { + * // imaginary DB queries + * return db.queryRefreshToken({refresh_token: refreshToken}) + * .then(function(token) { + * return Promise.all([ + * token, + * db.queryClient({id: token.client_id}), + * db.queryUser({id: token.user_id}) + * ]); + * }) + * .spread(function(token, client, user) { + * return { + * refreshToken: token.refresh_token, + * refreshTokenExpiresAt: token.expires_at, + * scope: token.scope, + * client: client, // with 'id' property + * user: user + * }; + * }); + * } + */ + async getRefreshToken(refreshToken) { + throw new ServerError('getRefreshToken not implemented'); + } + + /** + * Invoked to retrieve an existing authorization code previously saved through `Model#saveAuthorizationCode() `. + * This model function is **required** if the `authorization_code` grant is used. + * **Invoked during:** + * - `authorization_code` grant + * + * + * @async + + * @instance + * @param authorizationCode {string} The authorization code to retrieve. + * @return {Promise} An `Object` representing the authorization code and associated data. + * @example + * function getAuthorizationCode(authorizationCode) { + * // imaginary DB queries + * return db.queryAuthorizationCode({authorization_code: authorizationCode}) + * .then(function(code) { + * return Promise.all([ + * code, + * db.queryClient({id: code.client_id}), + * db.queryUser({id: code.user_id}) + * ]); + * }) + * .spread(function(code, client, user) { + * return { + * authorizationCode: code.authorization_code, + * expiresAt: code.expires_at, + * redirectUri: code.redirect_uri, + * scope: code.scope, + * client: client, // with 'id' property + * user: user + * }; + * }); + * } + */ + async getAuthorizationCode(authorizationCode) { + throw new ServerError('getAuthorizationCode not implemented'); + } + + /** + * Invoked to save an authorization code. + * This model function is **required** if the `authorization_code` grant is used. + * + * **Invoked during:** + * - `authorization_code` grant + * + * @async + + * @instance + * @param code {object} The code to be saved. + * @param code.authorizationCode {string} The authorization code to be saved. + * @param code.expiresAt {Date} The expiry time of the authorization code. + * @param code.redirectUri {string} The redirect URI associated with the authorization code. + * @param code.scope {string[]} The authorized scope of the authorization code. + * @param client {ClientData} The client associated with the authorization code. + * @param user {object} The user associated with the authorization code. + * @return {Promise.} + * @fulfil {{ authorizationCode: string, expiresAt: Date, redirectUri: string,scope: string[],client: ClientData,user: object}} An `Object` representing the authorization code and associated data. `code.client` and `code.user` can carry additional properties that will be ignored by *oauth2-server*. + * @example + * function saveAuthorizationCode(code, client, user) { + * // imaginary DB queries + * let authCode = { + * authorization_code: code.authorizationCode, + * expires_at: code.expiresAt, + * redirect_uri: code.redirectUri, + * scope: code.scope, + * client_id: client.id, + * user_id: user.id + * }; + * return db.saveAuthorizationCode(authCode) + * .then(function(authorizationCode) { + * return { + * authorizationCode: authorizationCode.authorization_code, + * expiresAt: authorizationCode.expires_at, + * redirectUri: authorizationCode.redirect_uri, + * scope: authorizationCode.scope, + * client: {id: authorizationCode.client_id}, + * user: {id: authorizationCode.user_id} + * }; + * }); + * } + */ + async saveAuthorizationCode(code, client, user) { + throw new ServerError('saveAuthorizationCode not implemented'); + } + + /** + * Invoked to revoke a refresh token. + * This model function is **required** if the `refresh_token` grant is used. + * **Invoked during:** + * - `refresh_token` grant + * + * **Remarks:** + * `token` is the refresh token object previously obtained through `Model#getRefreshToken() `. + * + * @async + + * @param token {RefreshTokenData} + * @return {Promise} Return `true` if the revocation was successful or `false` if the refresh token could not be found. + * @example + * function revokeToken(token) { + * // imaginary DB queries + * return db.deleteRefreshToken({refresh_token: token.refreshToken}) + * .then(function(refreshToken) { + * return !!refreshToken; + * }); + * } + */ + async revokeToken(token) { + throw new ServerError('revokeToken not implemented'); + } + + /** + * Invoked to revoke an authorization code. + * This model function is **required** if the `authorization_code` grant is used. + * + * **Invoked during:** + * - `authorization_code` grant + * + * **Remarks:** + * `code` is the authorization code object previously obtained through {@link Model#getAuthorizationCode}. + * + * @async + * @method + * @param code {AuthorizationCodeData} + * @return {Promise} Return `true` if the revocation was successful or `false` if the authorization code could not be found. + */ + async revokeAuthorizationCode(code) { + throw new ServerError('revokeAuthorizationCode not implemented'); + } + + /** + * Invoked during request authentication to check if the provided access token was authorized the requested scopes. + * + * This model function is **required** if scopes are used with `OAuth2Server#authenticate() ` + * but it's never called, if you provide your own `authenticateHandler` to the options. + * + * **Invoked during:** + * - request authentication + * + * **Remarks:** + * - `token` is the access token object previously obtained through `Model#getAccessToken() `. + * - `scope` is the required scope as given to `OAuth2Server#authenticate() ` as `options.scope`. + * + * @async + + * @param accessToken {AccessTokenData} + * @param scope {string[]} The required scopes. + * @return {Promise} Returns `true` if the access token passes, `false` otherwise. + * @example + * function verifyScope(token, requestedScopes) { + * if (!token.scope) { + * return false; + * } + * let authorizedScopes = token.scope; + * return requestedScopes.every(s => authorizedScopes.includes(s)); + * } + */ + async verifyScope(accessToken, scope) { + throw new ServerError('verifyScope not implemented'); + } + + + /*------------------------------------------------------------------------- + | OPTIONAL + *------------------------------------------------------------------------- + | The following functions are entirely optional + */ + + /** + * Invoked to generate a new access token. + * This model function is **optional**. + * + * If not implemented, a default handler is used that generates access tokens consisting of 40 characters in the range of `a..z0..9`. + * [RFC 6749 Appendix A.12](https://www.rfc-editor.org/rfc/rfc6749#appendix-A.12>) specifies that access tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + * + * **Invoked during:** + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * **Remarks:** + * - `client` is the object previously obtained through `Model#getClient() `. + * - `user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant). + * + * @async + + * @param client {object} The client the access token is generated for + * @param user {object} The user the access token is generated for. + * @param scope {string[]?} The scopes associated with the token. Can be `null` + * @return {Promise.} A `String` to be used as access token. + */ + async generateAccessToken(client, user, scope) { + throw new ServerError('generateAccessToken not implemented'); + } + + /** + * Invoked to generate a new refresh token. + * + * This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of `a..z0..9`. + * [RFC 6749 Appendix A.17](https://www.rfc-editor.org/6749#appendix-A.17) specifies that refresh tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + * + * **Invoked during:** + * + * - `authorization_code` grant + * - `refresh_token` grant + * - `password` grant + * + * **Remarks:** + * + * `client` is the object previously obtained through `Model#getClient() `. + * + * `user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant). + * + * @async + + * @param client {object} The client the refresh token is generated for + * @param user {object} The user the refresh token is generated for. + * @param scope {string[]?} The scopes associated with the refresh token. Can be `null` + * @return {Promise} A `String` to be used as refresh token. + */ + async generateRefreshToken(client, user, scope) { + throw new ServerError('generateRefreshToken not implemented'); + } + + /** + * Invoked to generate a new authorization code. + * This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of `a..z0..9`. + * [RFC 6749 Appendix A.11](https://www.rfc-editor.org/6749#appendix-A.11) specifies that authorization codes must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). + * + * **Invoked during:** + * - `authorization_code` grant + *>` + * + + * @async + * @param client {object} The client the authorization code is generated for. + * @param user {object} The user the authorization code is generated for. + * @param scope {string[]?} The scopes associated with the authorization code. Can be `null`. + * @return {Promise} A `String` to be used as authorization code. + * + */ + async generateAuthorizationCode(client, user, scope) { + throw new ServerError('generateAuthorizationCode not implemented'); + } + + /** + * Invoked to check if the requested `scope` is valid for a particular `client`/`user` combination. + * + * This model function is **optional**. If not implemented, any scope is accepted. + * + * **Invoked during:** + * + * - `authorization_code` grant + * - `client_credentials` grant + * - `password` grant + * + * **Remarks:** + * + * `user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant) or `Model#getUser() ` (password grant). + * + * `client` is the object previously obtained through `Model#getClient ` (all grants). + * + * You can decide yourself whether you want to reject or accept partially valid scopes by simply filtering out invalid scopes and returning only the valid ones. + * + * @async + + * @param user {object} The associated user. + * @param client {ClientData} The associated client. + * @param scope {string[]} The scopes to validate. + * @return {Promise} Validated scopes to be used or a falsy value to reject the requested scopes. + * @example + * // To reject invalid or only partially valid scopes: + * const VALID_SCOPES = ['read', 'write']; + * function validateScope(user, client, scope) { + * if (!scope.every(s => VALID_SCOPES.indexOf(s) >= 0)) { + * return false; + * } + * return scope; + * } + * @example + * // To accept partially valid scopes: + * const VALID_SCOPES = ['read', 'write']; + * function validateScope(user, client, scope) { + * return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); + * } + */ + async validateScope(user, client, scope) { + throw new ServerError('validateScope not implemented'); + } + + /** + * Invoked to check if the provided `redirectUri` is valid for a particular `client`. + * This model function is **optional**. If not implemented, the `redirectUri` should be included in the provided `redirectUris` of the client. + * + * **Invoked during:** + * - `authorization_code` grant + * + * **Remarks:** + * When implementing this method you should take care of possible security risks related to `redirectUri`. + * See: https://datatracker.ietf.org/doc/html/rfc6819 + * (Section-5.2.3.5 is implemented by default). + * + * @async + + * @param redirectUri {string} The redirect URI to validate + * @param client {object} The associated client. + * @return {Promise} Returns `true` if the `redirectUri` is valid, `false` otherwise. + */ + async validateRedirectUri(redirectUri, client) { + throw new ServerError('validateRedirectUri not implemented'); + } +} + + +module.exports = Model; \ No newline at end of file diff --git a/lib/models/token-model.js b/lib/models/token-model.js index 40dee37c..ff24ae81 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -1,13 +1,16 @@ 'use strict'; -/** +/* * Module dependencies. */ const InvalidArgumentError = require('../errors/invalid-argument-error'); const { getLifetimeFromExpiresAt } = require('../utils/date-util'); /** - * The core model attributes allowed when allowExtendedTokenAttributes is false. + * @private + * @typedef modelAttributes + * @type {Set} + * @description The core model attributes allowed when `allowExtendedTokenAttributes` is `false`. */ const modelAttributes = new Set([ 'accessToken', @@ -19,7 +22,16 @@ const modelAttributes = new Set([ 'user' ]); +/** + * @class + * @classDesc + */ class TokenModel { + /** + * @constructor + * @param data + * @param options + */ constructor(data = {}, options = {}) { const { accessToken, diff --git a/lib/pkce/pkce.js b/lib/pkce/pkce.js index e7603d98..dbf95378 100644 --- a/lib/pkce/pkce.js +++ b/lib/pkce/pkce.js @@ -1,77 +1,85 @@ 'use strict'; -/** +/* * Module dependencies. */ const { base64URLEncode } = require('../utils/string-util'); const { createHash } = require('../utils/crypto-util'); const codeChallengeRegexp = /^([a-zA-Z0-9.\-_~]){43,128}$/; + /** - * Export `TokenUtil`. + * @module pkce */ -const pkce = { - /** - * Return hash for code-challenge method-type. - * - * @param method {String} the code challenge method - * @param verifier {String} the code_verifier - * @return {String|undefined} - */ - getHashForCodeChallenge: function({ method, verifier }) { - // to prevent undesired side-effects when passing some wird values - // to createHash or base64URLEncode we first check if the values are right - if (pkce.isValidMethod(method) && typeof verifier === 'string' && verifier.length > 0) { - if (method === 'plain') { - return verifier; - } - if (method === 'S256') { - const hash = createHash({ data: verifier }); - return base64URLEncode(hash); - } +/** + * Return hash for code-challenge method-type. + * + * @function + * @param method {String} the code challenge method + * @param verifier {String} the code_verifier + * @return {String|undefined} + */ +function getHashForCodeChallenge({ method, verifier }) { + // to prevent undesired side-effects when passing some weird values + // to createHash or base64URLEncode we first check if the values are right + if (isValidMethod(method) && typeof verifier === 'string' && verifier.length > 0) { + if (method === 'plain') { + return verifier; } - }, - /** - * Check if the request is a PCKE request. We assume PKCE if grant type is - * 'authorization_code' and code verifier is present. - * - * @param grantType {String} - * @param codeVerifier {String} - * @return {boolean} - */ - isPKCERequest: function ({ grantType, codeVerifier }) { - return grantType === 'authorization_code' && !!codeVerifier; - }, + if (method === 'S256') { + const hash = createHash({ data: verifier }); + return base64URLEncode(hash); + } + } +} - /** - * Matches a code verifier (or code challenge) against the following criteria: - * - * code-verifier = 43*128unreserved - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * ALPHA = %x41-5A / %x61-7A - * DIGIT = %x30-39 - * - * @see: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 - * @param codeChallenge {String} - * @return {Boolean} - */ - codeChallengeMatchesABNF: function (codeChallenge) { - return typeof codeChallenge === 'string' && - !!codeChallenge.match(codeChallengeRegexp); - }, +/** + * Matches a code verifier (or code challenge) against the following criteria: + * + * code-verifier = 43*128unreserved + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * ALPHA = %x41-5A / %x61-7A + * DIGIT = %x30-39 + * + * @see: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + * @param codeChallenge {String} + * @return {Boolean} + */ +function codeChallengeMatchesABNF (codeChallenge) { + return typeof codeChallenge === 'string' && + !!codeChallenge.match(codeChallengeRegexp); +} + + +/** + * Check if the request is a PCKE request. We assume PKCE if grant type is + * 'authorization_code' and code verifier is present. + * + * @param grantType {String} + * @param codeVerifier {String} + * @return {boolean} + */ +function isPKCERequest ({ grantType, codeVerifier }) { + return grantType === 'authorization_code' && !!codeVerifier; +} - /** - * Checks if the code challenge method is one of the supported methods - * 'sha256' or 'plain' - * - * @param method {String} - * @return {boolean} - */ - isValidMethod: function (method) { - return method === 'S256' || method === 'plain'; - } -}; -module.exports = pkce; +/** + * Checks if the code challenge method is one of the supported methods + * 'sha256' or 'plain' + * + * @param method {String} + * @return {boolean} + */ +function isValidMethod (method) { + return method === 'S256' || method === 'plain'; +} + +module.exports = { + getHashForCodeChallenge, + codeChallengeMatchesABNF, + isPKCERequest, + isValidMethod +}; diff --git a/lib/request.js b/lib/request.js index 7ac01a44..f42960ca 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,13 +1,34 @@ 'use strict'; -/** +/* * Module dependencies. */ - const InvalidArgumentError = require('./errors/invalid-argument-error'); +/* type-is: https://github.com/jshttp/type-is */ const typeis = require('type-is'); +/** + * Wrapper for webserver's request. + * Used to decouple this package from the webserver's + * request signature. + * @class + * @example + * function (req, res, next) { + * // most webservers follow a similar structure + * const response = new Request(req); + * } + */ class Request { + /** + * Creates a new request instance + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param query {object} key-value object of query parameters + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + * @throws {InvalidArgumentError} if one of headers, method or query are missing. + */ constructor({ headers, method, query, body, ...otherOptions } = {}) { if (!headers) { throw new InvalidArgumentError('Missing parameter: `headers`'); @@ -40,8 +61,9 @@ class Request { } /** - * Get a request header. + * Get a request header (case-insensitive). * @param {String} field + * @return {string} */ get(field) { return this.headers[field.toLowerCase()]; @@ -49,7 +71,8 @@ class Request { /** * Check if the content-type matches any of the given mime types. - * @param {...String|Array} types + * @param {...string[]} types + * @return {boolean} */ is(...types) { return typeis(this, types.flat()) || false; diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index 6311d22a..ccafdd22 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -1,13 +1,22 @@ 'use strict'; -/** +/* * Module dependencies. */ const InvalidArgumentError = require('../errors/invalid-argument-error'); const url = require('url'); +/** + * @class + * @classDesc + */ class CodeResponseType { + /** + * @constructor + * @param code + * @throws {InvalidArgumentError} if {code} is missing + */ constructor(code) { if (!code) { throw new InvalidArgumentError('Missing parameter: `code`'); @@ -16,6 +25,11 @@ class CodeResponseType { this.code = code; } + /** + * @param redirectUri + * @return {UrlWithParsedQuery} + * @throws {InvalidArgumentError} if redirectUri is missing + */ buildRedirectUri(redirectUri) { if (!redirectUri) { throw new InvalidArgumentError('Missing parameter: `redirectUri`'); diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js index cd6891b4..e06f9fc3 100644 --- a/lib/response-types/token-response-type.js +++ b/lib/response-types/token-response-type.js @@ -1,12 +1,20 @@ 'use strict'; -/** +/* * Module dependencies. */ const ServerError = require('../errors/server-error'); +/** + * @class + * @classDesc + */ class TokenResponseType { + /** + * @constructor + * @throws {ServerError} not implemented yet + */ constructor() { throw new ServerError('Not implemented.'); } diff --git a/lib/response.js b/lib/response.js index 23725963..5afec5c8 100644 --- a/lib/response.js +++ b/lib/response.js @@ -1,6 +1,26 @@ 'use strict'; +/** + * Wrapper for webserver's response object. + * Used to decouple this package from the webserver's + * response signature. + * @class + * @example + * function (req, res, next) { + * // most webservers follow a similar structure + * const response = new Response(res); + * } + */ class Response { + + /** + * Create a new Response instance. + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + */ constructor({ headers = {}, body = {}, ...otherOptions } = {}) { this.status = 200; this.body = body; @@ -21,6 +41,8 @@ class Response { /** * Get a response header. + * @param field {string} the field to access, case-insensitive + * @return {string|undefined} */ get(field) { return this.headers[field.toLowerCase()]; @@ -28,6 +50,7 @@ class Response { /** * Redirect response. + * @param url {string} the url to redirect to */ redirect(url) { this.set('Location', url); @@ -36,6 +59,8 @@ class Response { /** * Set a response header. + * @param field {string} the name of the header field, case-insensitive + * @param value {string} the new value of the header field */ set(field, value) { this.headers[field.toLowerCase()] = value; diff --git a/lib/server.js b/lib/server.js index a2e31878..34658335 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,19 +1,65 @@ 'use strict'; -/** +/* * Module dependencies. */ - const AuthenticateHandler = require('./handlers/authenticate-handler'); const AuthorizeHandler = require('./handlers/authorize-handler'); const InvalidArgumentError = require('./errors/invalid-argument-error'); const TokenHandler = require('./handlers/token-handler'); +// we require the model only for JSDoc linking +require('./model'); /** - * Constructor. + * @class + * @classDesc The main OAuth2 server class. + * @example + * const OAuth2Server = require('@node-oauth/oauth2-server'); */ - class OAuth2Server { + + /** + * Instantiates `OAuth2Server` using the supplied model. + * **Remarks:** + * - Any valid option for {@link OAuth2Server#authenticate}, {@link OAuth2Server#authorize} and {@link OAuth2Server#token} can be passed to the constructor as well. + * - The supplied options will be used as default for the other methods. + * + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * + * @throws {InvalidArgumentError} if the model is missing + * @return {OAuth2Server} A new `OAuth2Server` instance. + * @example + * // Basic usage: + * const oauth = new OAuth2Server({ + * model: require('./model') + * }); + * @example + * // Advanced example with additional options: + * const oauth = new OAuth2Server({ + * model: require('./model'), + * allowBearerTokensInQueryString: true, + * accessTokenLifetime: 4 * 60 * 60 + * }); + */ constructor (options) { options = options || {}; @@ -25,9 +71,32 @@ class OAuth2Server { } /** - * Authenticate a token. - */ - + * Authenticates a request. + * @function + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {UnauthorizedRequestError} The protected resource request failed authentication. + * @return {Promise.} A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authenticateHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authenticate(request, response, options) + * .then(function(token) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ authenticate (request, response, options) { options = Object.assign({ addAcceptedScopesHeader: true, @@ -39,9 +108,60 @@ class OAuth2Server { } /** - * Authorize a request. - */ - + * Authorizes a token request. + * **Remarks:** + * + * If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. + * + * In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. + * The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. + * If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. + * + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return // get authenticated user; + * } + * }; + * ``` + * When working with a session-based login mechanism, the handler can simply look like this: + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return request.session.user; + * } + * }; + * ``` + * + * @function + * @param request {Request} the Request instance object + * @param request.query.allowed {string=} `'false'` to deny the authorization request (see remarks section). + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * @throws {AccessDeniedError} The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). + * @return {Promise.} A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authorizeHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authorize(request, response, options) + * .then(function(code) { + * res.locals.oauth = {code: code}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ authorize (request, response, options) { options = Object.assign({ allowEmptyState: false, @@ -52,9 +172,57 @@ class OAuth2Server { } /** - * Create a token. - */ - + * Retrieves a new token for an authorized token request. + * **Remarks:** + * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. + * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. + * ```js + * let options = { + * // ... + * // Allow token requests using the password grant to not include a client_secret. + * requireClientAuthentication: {password: false} + * }; + * ``` + * `options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: + * ```js + * let options = { + * // ... + * extendedGrantTypes: { + * 'urn:foo:bar:baz': MyGrantType + * } + * }; + * ``` + * For information on how to implement a handler for a custom grant type see the extension grants. + * @function + * @param request {Request} the Request instance object + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * @return {Promise.} A `Promise` that resolves to the token object returned from the model's `saveToken` method. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @throws {InvalidGrantError} The access token request was invalid or not authorized. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function tokenHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.token(request, response, options) + * .then(function(code) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ token (request, response, options) { options = Object.assign({ accessTokenLifetime: 60 * 60, // 1 hour. @@ -67,8 +235,4 @@ class OAuth2Server { } } -/** - * Export constructor. - */ - module.exports = OAuth2Server; diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index 899f9a86..373d8731 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -1,16 +1,25 @@ 'use strict'; -/** +/* * Module dependencies. */ const InvalidArgumentError = require('../errors/invalid-argument-error'); /** - * Constructor. + * @class + * @classDesc */ - class BearerTokenType { + + /** + * @constructor + * @param accessToken + * @param accessTokenLifetime + * @param refreshToken + * @param scope + * @param customAttributes + */ constructor(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) { if (!accessToken) { throw new InvalidArgumentError('Missing parameter: `accessToken`'); @@ -57,8 +66,4 @@ class BearerTokenType { } } -/** - * Export constructor. - */ - module.exports = BearerTokenType; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js index 2d90fbe8..02a90114 100644 --- a/lib/token-types/mac-token-type.js +++ b/lib/token-types/mac-token-type.js @@ -1,23 +1,19 @@ 'use strict'; -/** - * Module dependencies. - */ - const ServerError = require('../errors/server-error'); /** - * Constructor. + * @class + * @classDesc */ - class MacTokenType { + /** + * @constructor + * @throws {ServerError} not yet implemented + */ constructor() { throw new ServerError('Not implemented.'); } } -/** - * Export constructor. - */ - module.exports = MacTokenType; diff --git a/lib/utils/date-util.js b/lib/utils/date-util.js index 4071a11f..4fe32f97 100644 --- a/lib/utils/date-util.js +++ b/lib/utils/date-util.js @@ -1,6 +1,13 @@ 'use strict'; /** + * @module DateUtil + */ + +/** + * Returns the remaining seconds of expiration from now. + * If the value is less than or equal zero, then it is considered expired. + * @function * @param expiresAt {Date} The date at which something (e.g. a token) expires. * @return {number} The number of seconds until the expiration date. */ diff --git a/lib/utils/scope-util.js b/lib/utils/scope-util.js index a067fdc0..5ed4bbd3 100644 --- a/lib/utils/scope-util.js +++ b/lib/utils/scope-util.js @@ -2,24 +2,39 @@ const isFormat = require('@node-oauth/formats'); const InvalidScopeError = require('../errors/invalid-scope-error'); const whiteSpace = /\s+/g; -module.exports = { - parseScope: function (requestedScope) { - if (requestedScope == null) { - return undefined; - } +/** + * @module ScopeUtil + */ - if (typeof requestedScope !== 'string') { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } +/** + * Utility to parse and validate a scope string. + * Uses `isFormat` from {@link https://github.com/node-oauth/formats} to + * validate scopes against `nqchar` format. + * + * @function + * @param requestedScope {string|undefined|null} + * @throws {InvalidScopeError} if the type is not null, undefined or a string. + * @return {undefined|string[]} + * @see {https://github.com/node-oauth/formats} + */ +function parseScope (requestedScope) { + if (requestedScope == null) { + return undefined; + } - // XXX: this prevents spaced-only strings to become - // treated as valid nqchar by making them empty strings - requestedScope = requestedScope.trim(); + if (typeof requestedScope !== 'string') { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } - if(!isFormat.nqschar(requestedScope)) { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } + // XXX: this prevents spaced-only strings to become + // treated as valid nqchar by making them empty strings + requestedScope = requestedScope.trim(); - return requestedScope.split(whiteSpace); + if(!isFormat.nqschar(requestedScope)) { + throw new InvalidScopeError('Invalid parameter: `scope`'); } -}; + + return requestedScope.split(whiteSpace); +} + +module.exports = { parseScope }; diff --git a/lib/utils/string-util.js b/lib/utils/string-util.js index 464bd074..2835b6d6 100644 --- a/lib/utils/string-util.js +++ b/lib/utils/string-util.js @@ -1,19 +1,20 @@ 'use strict'; /** - * Export `StringUtil`. + * @module StringUtil */ -module.exports = { - /** - * - * @param str - * @return {string} - */ - base64URLEncode: function(str) { - return str.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - } -}; +/** + * Encodes a string to a valid base64 string that + * can be used as URL component. + * @param str + * @return {string} + */ +function base64URLEncode(str) { + return str.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +module.exports = { base64URLEncode }; diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js index 52487567..5c9e2392 100644 --- a/lib/utils/token-util.js +++ b/lib/utils/token-util.js @@ -1,30 +1,29 @@ 'use strict'; +const randomBytes = require('crypto').randomBytes; + /** - * Module dependencies. + * @module TokenUtil */ -const randomBytes = require('crypto').randomBytes; - /** - * Export `TokenUtil`. + * Generates random token as 32 byte hex string. + * @function + * @async + * @return {Promise} */ +function generateRandomToken() { + return new Promise((resolve, reject) => { + randomBytes(32, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.toString('hex')); + } + }); + }); +} module.exports = { - - /** - * Generate random token. - */ - - generateRandomToken: function() { - return new Promise((resolve, reject) => { - randomBytes(32, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data.toString('hex')); - } - }); - }); - } + generateRandomToken }; diff --git a/package-lock.json b/package-lock.json index 4eddd526..1edc0c54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,334 @@ "devDependencies": { "chai": "6.2.2", "eslint": "8.57.1", + "jsdoc-to-markdown": "^9.1.3", "mocha": "11.7.5", "nyc": "17.1.0", - "sinon": "21.0.1" + "sinon": "21.0.1", + "vitepress": "^2.0.0-alpha.15" }, "engines": { "node": ">=16.0.0" } }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.23.tgz", + "integrity": "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", + "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.118", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.118.tgz", + "integrity": "sha512-K/5VVEGTIu9SWrdQ0s/11OldFU8IjprDzeE6TaC2fOcQWhG7dGVGl9H8Z32QBHzdfJyMhFUxEyFKSOgA2j9+VQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.19", + "ai": "5.0.116", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", + "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", + "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", + "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", + "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", + "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", + "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", + "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", + "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", + "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", + "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", + "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", + "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", + "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", + "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -251,9 +571,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -284,13 +604,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -345,186 +665,723 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { + "node_modules/@docsearch/core": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.4.0.tgz", + "integrity": "sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, + "license": "MIT", "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "node_modules/@docsearch/css": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.4.0.tgz", + "integrity": "sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==", "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.4.0.tgz", + "integrity": "sha512-vCiKzjYD54bugUIMZA6YzuLDilkD3TNH/kfbvqsnzxiLTMu8F13psD+hdMSEOn7j+dFJOaf49fZ+gwr+rXctMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "4.4.0", + "htm": "3.1.1" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@docsearch/react": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.4.0.tgz", + "integrity": "sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.4.0", + "@docsearch/css": "4.4.0", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10.10.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "funding": { + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.64", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.64.tgz", + "integrity": "sha512-SMmm//tjZBvHnT0EAzZLnBTL6bukSkncM0pwkOXjr0FsAeCqjQtqoxBR0Mp+PazIJjXJKHm1Ju0YgnCIPOodJg==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, @@ -533,267 +1390,1044 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@node-oauth/formats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@node-oauth/formats/-/formats-1.0.0.tgz", + "integrity": "sha512-DwSbLtdC8zC5B5gTJkFzJj5s9vr9SGzOgQvV9nH7tUVuMSScg0EswAczhjIapOmH3Y8AyP7C4Jv7b8+QJObWZA==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.20.0.tgz", + "integrity": "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.20.0.tgz", + "integrity": "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz", + "integrity": "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz", + "integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz", + "integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.20.0.tgz", + "integrity": "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.20.0", + "@shikijs/types": "3.20.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz", + "integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz", + "integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=4" } }, - "node_modules/@istanbuljs/load-nyc-config": { + "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, + "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@types/unist": "*" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@types/linkify-it": "^5", + "@types/mdurl": "^2" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "@types/unist": "*" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">= 20" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", "dev": true, + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@rolldown/pluginutils": "1.0.0-beta.53" }, "engines": { - "node": ">=6" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@vue/devtools-api": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", + "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", "dev": true, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.5" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@vue/devtools-kit": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", + "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", "dev": true, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@vue/devtools-shared": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", + "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "rfdc": "^1.4.1" } }, - "node_modules/@node-oauth/formats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@node-oauth/formats/-/formats-1.0.0.tgz", - "integrity": "sha512-DwSbLtdC8zC5B5gTJkFzJj5s9vr9SGzOgQvV9nH7tUVuMSScg0EswAczhjIapOmH3Y8AyP7C4Jv7b8+QJObWZA==" + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", "dev": true, - "engines": { - "node": ">= 8" + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "vue": "3.5.26" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } + "license": "MIT" }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@vueuse/core": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", + "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", "dev": true, + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz", - "integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==", + "node_modules/@vueuse/integrations": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz", + "integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==", "dev": true, + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.1" + "@vueuse/core": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "node_modules/@vueuse/metadata": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", + "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "node_modules/@vueuse/shared": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", + "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", "dev": true, - "engines": { - "node": ">=4" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -828,6 +2462,25 @@ "node": ">=8" } }, + "node_modules/ai": { + "version": "5.0.116", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.116.tgz", + "integrity": "sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.23", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -844,6 +2497,32 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/algoliasearch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", + "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.12.2", + "@algolia/client-abtesting": "5.46.2", + "@algolia/client-analytics": "5.46.2", + "@algolia/client-common": "5.46.2", + "@algolia/client-insights": "5.46.2", + "@algolia/client-personalization": "5.46.2", + "@algolia/client-query-suggestions": "5.46.2", + "@algolia/client-search": "5.46.2", + "@algolia/ingestion": "1.46.2", + "@algolia/monitoring": "1.46.2", + "@algolia/recommend": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -892,6 +2571,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -909,6 +2598,23 @@ "node": ">= 0.8" } }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -919,6 +2625,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -957,6 +2676,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cache-point": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz", + "integrity": "sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -1010,6 +2750,30 @@ } ] }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1035,6 +2799,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1093,6 +2895,67 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/common-sequence": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-3.0.0.tgz", + "integrity": "sha512-g/CgSYk93y+a1IKm50tKl7kaT/OjjTYVQlEbUlt/49ZLV1mcKpUU7iyDiqTAeLdb4QDtQfq3ako8y8v//fzrWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1105,6 +2968,26 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/config-master": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", + "integrity": "sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-back": "^2.0.1" + } + }, + "node_modules/config-master/node_modules/walk-back": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-2.0.1.tgz", + "integrity": "sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -1120,6 +3003,22 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1135,6 +3034,23 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/current-module-paths": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/current-module-paths/-/current-module-paths-1.1.3.tgz", + "integrity": "sha512-7AH+ZTRKikdK4s1RmY0l6067UD/NZc7p3zZVZxvmnH80G31kr0y0W0E6ibYM4IS01MEm8DiC5FnTcgcgkbFHoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -1182,6 +3098,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -1192,6 +3132,46 @@ "node": ">=0.3.1" } }, + "node_modules/dmd": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-7.1.1.tgz", + "integrity": "sha512-Ap2HP6iuOek7eShReDLr9jluNJm9RMZESlt29H/Xs1qrVMkcS9X6m5h1mBC56WMxNiSo0wvjGICmZlYUSFjwZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "cache-point": "^3.0.0", + "common-sequence": "^3.0.0", + "file-set": "^5.2.2", + "handlebars": "^4.7.8", + "marked": "^4.3.0", + "walk-back": "^5.1.1" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/dmd/node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1222,12 +3202,67 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -1395,6 +3430,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1404,12 +3446,52 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1431,6 +3513,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1443,6 +3543,41 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-set": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", + "integrity": "sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -1460,6 +3595,24 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1505,6 +3658,16 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/focus-trap": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.0.tgz", + "integrity": "sha512-DJJDHpEgoSbP8ZE1MNeU2IzCpfFyFdNZZRilqmfH2XiQsPK6PtD8AfJqWzEBudUQB2yHwZc5iq54rjTaGQ+ljw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.3.0" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -1544,6 +3707,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1682,6 +3860,28 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1716,6 +3916,44 @@ "node": ">=8" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1725,12 +3963,37 @@ "he": "bin/he" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -1821,6 +4084,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -1869,6 +4142,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -2028,6 +4314,142 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-api": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz", + "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "cache-point": "^3.0.1", + "current-module-paths": "^1.1.2", + "file-set": "^5.3.0", + "jsdoc": "^4.0.4", + "object-to-spawn-args": "^2.0.1", + "walk-back": "^5.1.1" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/jsdoc-parse": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.5.tgz", + "integrity": "sha512-8JaSNjPLr2IuEY4Das1KM6Z4oLHZYUnjRrr27hKSa78Cj0i5Lur3DzNnCkz+DfrKBDoljGMoWOiBVQbtUZJBPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.1", + "sort-array": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdoc-to-markdown": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-9.1.3.tgz", + "integrity": "sha512-i9wi+6WHX0WKziv0ar88T8h7OmxA0LWdQaV23nY6uQyKvdUPzVt0o6YAaOceFuKRF5Rvlju5w/KnZBfdpDAlnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.3", + "config-master": "^3.1.0", + "dmd": "^7.1.1", + "jsdoc-api": "^9.3.5", + "jsdoc-parse": "^6.2.5", + "walk-back": "^5.1.1" + }, + "bin": { + "jsdoc2md": "bin/cli.js" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2046,6 +4468,13 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2079,6 +4508,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2092,6 +4531,16 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2107,6 +4556,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -2129,44 +4592,145 @@ "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "dev": true, - "dependencies": { - "yallist": "^3.0.2" + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, + "license": "MIT", "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, - "bin": { - "semver": "bin/semver.js" - } + "license": "MIT" }, "node_modules/media-typer": { "version": "1.1.0", @@ -2177,6 +4741,137 @@ "node": ">= 0.8" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2210,6 +4905,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -2219,6 +4924,33 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "11.7.5", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", @@ -2308,12 +5040,38 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -2549,6 +5307,16 @@ "node": ">=6" } }, + "node_modules/object-to-spawn-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", + "integrity": "sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2558,6 +5326,25 @@ "wrappy": "1" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2708,6 +5495,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2715,6 +5509,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -2779,6 +5586,35 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2800,6 +5636,17 @@ "node": ">=8" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2809,6 +5656,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2838,6 +5695,17 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2852,6 +5720,33 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -2879,6 +5774,16 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2898,6 +5803,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2935,6 +5847,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2963,6 +5917,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3011,6 +5973,23 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.20.0.tgz", + "integrity": "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.20.0", + "@shikijs/engine-javascript": "3.20.0", + "@shikijs/engine-oniguruma": "3.20.0", + "@shikijs/langs": "3.20.0", + "@shikijs/themes": "3.20.0", + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3043,6 +6022,28 @@ "node": ">=0.3.1" } }, + "node_modules/sort-array": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.1.1.tgz", + "integrity": "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "^0.1.1" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3052,6 +6053,27 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -3069,6 +6091,16 @@ "node": ">=8" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3104,6 +6136,21 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3150,6 +6197,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3162,6 +6222,41 @@ "node": ">=8" } }, + "node_modules/swr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz", + "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -3203,6 +6298,60 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3259,6 +6408,117 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -3269,42 +6529,235 @@ "type": "opencollective", "url": "https://opencollective.com/browserslist" }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } - ], + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.15", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.15.tgz", + "integrity": "sha512-jhjSYd10Z6RZiKOa7jy0xMVf5NB5oSc/lS3bD/QoUc6V8PrvQR5JhC9104NEt6+oTGY/ftieVWxY9v7YI+1IjA==", + "dev": true, + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "@docsearch/css": "^4.3.2", + "@docsearch/js": "^4.3.2", + "@iconify-json/simple-icons": "^1.2.59", + "@shikijs/core": "^3.15.0", + "@shikijs/transformers": "^3.15.0", + "@shikijs/types": "^3.15.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.24", + "@vueuse/core": "^14.0.0", + "@vueuse/integrations": "^14.0.0", + "focus-trap": "^7.6.6", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.15.0", + "vite": "^7.2.2", + "vue": "^3.5.24" }, "bin": { - "update-browserslist-db": "cli.js" + "vitepress": "bin/vitepress.js" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vue": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "dev": true, + "license": "MIT", "dependencies": { - "punycode": "^2.1.0" + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/walk-back": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.1.tgz", + "integrity": "sha512-e/FRLDVdZQWFrAzU6Hdvpm7D7m2ina833gIKLptQykRK49mmCYHLHq7UqjPDbxbKLZkTkW1rFqbengdE3sLfdw==", "dev": true, - "bin": { - "uuid": "dist/bin/uuid" + "license": "MIT", + "engines": { + "node": ">=12.17" } }, "node_modules/which": { @@ -3337,6 +6790,23 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wordwrapjs": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/workerpool": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", @@ -3398,6 +6868,13 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3493,9 +6970,252 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } }, "dependencies": { + "@ai-sdk/gateway": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.23.tgz", + "integrity": "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg==", + "dev": true, + "requires": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@vercel/oidc": "3.0.5" + } + }, + "@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "dev": true, + "requires": { + "json-schema": "^0.4.0" + } + }, + "@ai-sdk/provider-utils": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", + "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "dev": true, + "requires": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + } + }, + "@ai-sdk/react": { + "version": "2.0.118", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.118.tgz", + "integrity": "sha512-K/5VVEGTIu9SWrdQ0s/11OldFU8IjprDzeE6TaC2fOcQWhG7dGVGl9H8Z32QBHzdfJyMhFUxEyFKSOgA2j9+VQ==", + "dev": true, + "requires": { + "@ai-sdk/provider-utils": "3.0.19", + "ai": "5.0.116", + "swr": "^2.2.5", + "throttleit": "2.1.0" + } + }, + "@algolia/abtesting": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", + "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "dev": true, + "requires": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "dev": true, + "requires": { + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "dev": true, + "requires": {} + }, + "@algolia/client-abtesting": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", + "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/client-analytics": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", + "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/client-common": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", + "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", + "dev": true + }, + "@algolia/client-insights": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", + "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/client-personalization": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", + "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/client-query-suggestions": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", + "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/client-search": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", + "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/ingestion": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", + "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/monitoring": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", + "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/recommend": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", + "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, + "@algolia/requester-browser-xhr": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", + "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2" + } + }, + "@algolia/requester-fetch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", + "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2" + } + }, + "@algolia/requester-node-http": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", + "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", + "dev": true, + "requires": { + "@algolia/client-common": "5.46.2" + } + }, "@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3670,9 +7390,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true }, "@babel/helper-validator-option": { @@ -3692,12 +7412,12 @@ } }, "@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "requires": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.5" } }, "@babel/template": { @@ -3737,15 +7457,236 @@ } } }, - "@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + } + }, + "@docsearch/core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.4.0.tgz", + "integrity": "sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==", + "dev": true, + "requires": {} + }, + "@docsearch/css": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.4.0.tgz", + "integrity": "sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==", + "dev": true + }, + "@docsearch/js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.4.0.tgz", + "integrity": "sha512-vCiKzjYD54bugUIMZA6YzuLDilkD3TNH/kfbvqsnzxiLTMu8F13psD+hdMSEOn7j+dFJOaf49fZ+gwr+rXctMw==", + "dev": true, + "requires": { + "@docsearch/react": "4.4.0", + "htm": "3.1.1" + } + }, + "@docsearch/react": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.4.0.tgz", + "integrity": "sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==", + "dev": true, + "requires": { + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.4.0", + "@docsearch/css": "4.4.0", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - } + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "dev": true, + "optional": true }, "@eslint-community/eslint-utils": { "version": "4.4.0", @@ -3808,6 +7749,21 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "@iconify-json/simple-icons": { + "version": "1.2.64", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.64.tgz", + "integrity": "sha512-SMmm//tjZBvHnT0EAzZLnBTL6bukSkncM0pwkOXjr0FsAeCqjQtqoxBR0Mp+PazIJjXJKHm1Ju0YgnCIPOodJg==", + "dev": true, + "requires": { + "@iconify/types": "*" + } + }, + "@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3980,9 +7936,9 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { @@ -3995,6 +7951,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, "@node-oauth/formats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@node-oauth/formats/-/formats-1.0.0.tgz", @@ -4006,75 +7971,561 @@ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "dev": true, + "optional": true + }, + "@shikijs/core": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.20.0.tgz", + "integrity": "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==", + "dev": true, + "requires": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "@shikijs/engine-javascript": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.20.0.tgz", + "integrity": "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==", + "dev": true, + "requires": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "@shikijs/engine-oniguruma": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz", + "integrity": "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==", + "dev": true, + "requires": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "@shikijs/langs": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz", + "integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==", + "dev": true, + "requires": { + "@shikijs/types": "3.20.0" + } + }, + "@shikijs/themes": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz", + "integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==", + "dev": true, + "requires": { + "@shikijs/types": "3.20.0" + } + }, + "@shikijs/transformers": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.20.0.tgz", + "integrity": "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g==", + "dev": true, + "requires": { + "@shikijs/core": "3.20.0", + "@shikijs/types": "3.20.0" + } + }, + "@shikijs/types": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz", + "integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==", + "dev": true, + "requires": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz", + "integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, + "@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "dev": true + }, + "@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "requires": { + "@rolldown/pluginutils": "1.0.0-beta.53" + } + }, + "@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "dev": true, + "requires": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "dev": true, + "requires": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "@vue/devtools-api": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", + "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", + "dev": true, + "requires": { + "@vue/devtools-kit": "^8.0.5" + } + }, + "@vue/devtools-kit": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", + "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "dev": true, + "requires": { + "@vue/devtools-shared": "^8.0.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "@vue/devtools-shared": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", + "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "dev": true, + "requires": { + "rfdc": "^1.4.1" + } + }, + "@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "dev": true, + "requires": { + "@vue/shared": "3.5.26" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" } }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", "dev": true, - "optional": true + "requires": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } }, - "@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", "dev": true, "requires": { - "type-detect": "4.0.8" + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" } }, - "@sinonjs/fake-timers": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz", - "integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==", + "@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "dev": true + }, + "@vueuse/core": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", + "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", "dev": true, "requires": { - "@sinonjs/commons": "^3.0.1" + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.1.0", + "@vueuse/shared": "14.1.0" } }, - "@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "@vueuse/integrations": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz", + "integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==", "dev": true, "requires": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" - }, - "dependencies": { - "type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true - } + "@vueuse/core": "14.1.0", + "@vueuse/shared": "14.1.0" } }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "@vueuse/metadata": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", + "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", "dev": true }, + "@vueuse/shared": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", + "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "dev": true, + "requires": {} + }, "acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -4098,6 +8549,18 @@ "indent-string": "^4.0.0" } }, + "ai": { + "version": "5.0.116", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.116.tgz", + "integrity": "sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ==", + "dev": true, + "requires": { + "@ai-sdk/gateway": "2.0.23", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@opentelemetry/api": "1.9.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4110,6 +8573,28 @@ "uri-js": "^4.2.2" } }, + "algoliasearch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", + "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", + "dev": true, + "requires": { + "@algolia/abtesting": "1.12.2", + "@algolia/client-abtesting": "5.46.2", + "@algolia/client-analytics": "5.46.2", + "@algolia/client-common": "5.46.2", + "@algolia/client-insights": "5.46.2", + "@algolia/client-personalization": "5.46.2", + "@algolia/client-query-suggestions": "5.46.2", + "@algolia/client-search": "5.46.2", + "@algolia/ingestion": "1.46.2", + "@algolia/monitoring": "1.46.2", + "@algolia/recommend": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + } + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4146,6 +8631,12 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4160,6 +8651,18 @@ "safe-buffer": "5.1.2" } }, + "birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4170,6 +8673,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -4188,6 +8700,15 @@ "update-browserslist-db": "^1.1.0" } }, + "cache-point": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz", + "integrity": "sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA==", + "dev": true, + "requires": { + "array-back": "^6.2.2" + } + }, "caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -4218,6 +8739,21 @@ "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", "dev": true }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true + }, "chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -4234,6 +8770,27 @@ "supports-color": "^7.1.0" } }, + "chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "requires": { + "chalk": "^4.1.2" + } + }, + "character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true + }, + "character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true + }, "chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4275,6 +8832,42 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true + }, + "command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + } + }, + "command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + } + }, + "common-sequence": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-3.0.0.tgz", + "integrity": "sha512-g/CgSYk93y+a1IKm50tKl7kaT/OjjTYVQlEbUlt/49ZLV1mcKpUU7iyDiqTAeLdb4QDtQfq3ako8y8v//fzrWQ==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -4287,6 +8880,23 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "config-master": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", + "integrity": "sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g==", + "dev": true, + "requires": { + "walk-back": "^2.0.1" + }, + "dependencies": { + "walk-back": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-2.0.1.tgz", + "integrity": "sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ==", + "dev": true + } + } + }, "content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -4298,6 +8908,15 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "requires": { + "is-what": "^5.2.0" + } + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4309,6 +8928,18 @@ "which": "^2.0.1" } }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "current-module-paths": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/current-module-paths/-/current-module-paths-1.1.3.tgz", + "integrity": "sha512-7AH+ZTRKikdK4s1RmY0l6067UD/NZc7p3zZVZxvmnH80G31kr0y0W0E6ibYM4IS01MEm8DiC5FnTcgcgkbFHoA==", + "dev": true + }, "debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -4339,12 +8970,50 @@ "strip-bom": "^4.0.0" } }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, + "devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "requires": { + "dequal": "^2.0.0" + } + }, "diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true }, + "dmd": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-7.1.1.tgz", + "integrity": "sha512-Ap2HP6iuOek7eShReDLr9jluNJm9RMZESlt29H/Xs1qrVMkcS9X6m5h1mBC56WMxNiSo0wvjGICmZlYUSFjwZQ==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "cache-point": "^3.0.0", + "common-sequence": "^3.0.0", + "file-set": "^5.2.2", + "handlebars": "^4.7.8", + "marked": "^4.3.0", + "walk-back": "^5.1.1" + }, + "dependencies": { + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4372,12 +9041,52 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "dev": true + }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -4493,18 +9202,54 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4526,6 +9271,13 @@ "reusify": "^1.0.4" } }, + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4535,6 +9287,25 @@ "flat-cache": "^3.0.4" } }, + "file-set": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", + "integrity": "sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "fast-glob": "^3.3.2" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -4546,6 +9317,13 @@ "pkg-dir": "^4.1.0" } }, + "find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "dev": true, + "requires": {} + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4579,6 +9357,15 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "focus-trap": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.0.tgz", + "integrity": "sha512-DJJDHpEgoSbP8ZE1MNeU2IzCpfFyFdNZZRilqmfH2XiQsPK6PtD8AfJqWzEBudUQB2yHwZc5iq54rjTaGQ+ljw==", + "dev": true, + "requires": { + "tabbable": "^6.3.0" + } + }, "foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -4601,6 +9388,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4699,6 +9493,19 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4723,18 +9530,64 @@ } } }, + "hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + } + }, + "hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "dev": true + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true + }, "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4800,6 +9653,12 @@ "is-extglob": "^2.1.1" } }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -4830,6 +9689,12 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -4952,6 +9817,94 @@ "argparse": "^2.0.1" } }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + } + } + }, + "jsdoc-api": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz", + "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "cache-point": "^3.0.1", + "current-module-paths": "^1.1.2", + "file-set": "^5.3.0", + "jsdoc": "^4.0.4", + "object-to-spawn-args": "^2.0.1", + "walk-back": "^5.1.1" + } + }, + "jsdoc-parse": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.5.tgz", + "integrity": "sha512-8JaSNjPLr2IuEY4Das1KM6Z4oLHZYUnjRrr27hKSa78Cj0i5Lur3DzNnCkz+DfrKBDoljGMoWOiBVQbtUZJBPw==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "find-replace": "^5.0.1", + "sort-array": "^5.0.0" + } + }, + "jsdoc-to-markdown": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-9.1.3.tgz", + "integrity": "sha512-i9wi+6WHX0WKziv0ar88T8h7OmxA0LWdQaV23nY6uQyKvdUPzVt0o6YAaOceFuKRF5Rvlju5w/KnZBfdpDAlnw==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.3", + "config-master": "^3.1.0", + "dmd": "^7.1.1", + "jsdoc-api": "^9.3.5", + "jsdoc-parse": "^6.2.5", + "walk-back": "^5.1.1" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4964,6 +9917,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4991,14 +9950,32 @@ "json-buffer": "3.0.1" } }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" } }, "locate-path": { @@ -5010,6 +9987,18 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -5041,6 +10030,15 @@ "yallist": "^3.0.2" } }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5058,11 +10056,138 @@ } } }, + "mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + } + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "requires": {} + }, + "marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "dev": true + }, + "mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "requires": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true + }, + "micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true + }, + "micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } + } + }, "mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -5085,12 +10210,36 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, "minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true }, + "minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, "mocha": { "version": "11.7.5", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", @@ -5161,12 +10310,24 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -5351,6 +10512,12 @@ } } }, + "object-to-spawn-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", + "integrity": "sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5360,6 +10527,23 @@ "wrappy": "1" } }, + "oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "dev": true + }, + "oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "dev": true, + "requires": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5470,12 +10654,24 @@ } } }, + "perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -5524,6 +10720,17 @@ } } }, + "postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5539,12 +10746,24 @@ "fromentries": "^1.2.0" } }, + "property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5560,12 +10779,43 @@ "safe-buffer": "^5.1.0" } }, + "react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "dev": true, + "peer": true + }, "readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true }, + "regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "requires": { + "regex-utilities": "^2.3.0" + } + }, + "regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "requires": { + "regex-utilities": "^2.3.0" + } + }, + "regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true + }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -5587,6 +10837,15 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5599,6 +10858,12 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5624,6 +10889,38 @@ } } }, + "rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5638,6 +10935,13 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "peer": true + }, "semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -5674,6 +10978,22 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "shiki": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.20.0.tgz", + "integrity": "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==", + "dev": true, + "requires": { + "@shikijs/core": "3.20.0", + "@shikijs/engine-javascript": "3.20.0", + "@shikijs/engine-oniguruma": "3.20.0", + "@shikijs/langs": "3.20.0", + "@shikijs/themes": "3.20.0", + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -5701,12 +11021,34 @@ } } }, + "sort-array": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.1.1.tgz", + "integrity": "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "typical": "^7.1.1" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -5721,6 +11063,12 @@ "which": "^2.0.1" } }, + "speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5749,6 +11097,16 @@ "strip-ansi": "^6.0.1" } }, + "stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "requires": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5779,6 +11137,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "requires": { + "copy-anything": "^4" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5788,6 +11155,32 @@ "has-flag": "^4.0.0" } }, + "swr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz", + "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==", + "dev": true, + "requires": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + } + }, + "tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "dev": true + }, + "table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "dev": true, + "requires": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5821,6 +11214,37 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5861,6 +11285,79 @@ "is-typedarray": "^1.0.0" } }, + "typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "dev": true + }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true + }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true + }, + "unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0" + } + }, + "unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + }, "update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -5880,12 +11377,99 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "requires": {} + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + } + }, + "vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "requires": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + } + }, + "vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "requires": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "fsevents": "~2.3.3", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + } + }, + "vitepress": { + "version": "2.0.0-alpha.15", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.15.tgz", + "integrity": "sha512-jhjSYd10Z6RZiKOa7jy0xMVf5NB5oSc/lS3bD/QoUc6V8PrvQR5JhC9104NEt6+oTGY/ftieVWxY9v7YI+1IjA==", + "dev": true, + "requires": { + "@docsearch/css": "^4.3.2", + "@docsearch/js": "^4.3.2", + "@iconify-json/simple-icons": "^1.2.59", + "@shikijs/core": "^3.15.0", + "@shikijs/transformers": "^3.15.0", + "@shikijs/types": "^3.15.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.24", + "@vueuse/core": "^14.0.0", + "@vueuse/integrations": "^14.0.0", + "focus-trap": "^7.6.6", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.15.0", + "vite": "^7.2.2", + "vue": "^3.5.24" + } + }, + "vue": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "walk-back": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.1.tgz", + "integrity": "sha512-e/FRLDVdZQWFrAzU6Hdvpm7D7m2ina833gIKLptQykRK49mmCYHLHq7UqjPDbxbKLZkTkW1rFqbengdE3sLfdw==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5907,6 +11491,18 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "wordwrapjs": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "dev": true + }, "workerpool": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", @@ -5953,6 +11549,12 @@ "typedarray-to-buffer": "^3.1.5" } }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6017,6 +11619,18 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true + }, + "zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true } } } diff --git a/package.json b/package.json index 3e773f5f..ea25a5b4 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ }, "devDependencies": { "chai": "6.2.2", - "mocha": "11.7.5", "eslint": "8.57.1", + "jsdoc-to-markdown": "^9.1.3", + "mocha": "11.7.5", "nyc": "17.1.0", - "sinon": "21.0.1" + "sinon": "21.0.1", + "vitepress": "^2.0.0-alpha.15" }, "license": "MIT", "engines": { @@ -48,7 +50,12 @@ "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha --watch 'test/**/*_test.js'", "test:coverage": "NODE_ENV=test nyc --reporter=html --reporter=lcov --reporter=text ./node_modules/.bin/mocha 'test/**/*_test.js'", "lint": "npx eslint .", - "lint:fix": "npx eslint . --fix" + "lint:fix": "npx eslint . --fix", + "docs:dev": "vitepress dev docs", + "docs:build": "npm run docs:setup && npm run docs:api && vitepress build docs", + "docs:preview": "vitepress preview docs", + "docs:api": "node docs/build-api.js lib docs/api", + "docs:setup": "cd docs/guide && ln -sf ../../CONTRIBUTING.md contributing.md" }, "repository": { "type": "git", diff --git a/test/helpers/model.js b/test/helpers/model.js index 4efe591f..12507bbf 100644 --- a/test/helpers/model.js +++ b/test/helpers/model.js @@ -1,3 +1,4 @@ +const Model = require('../../lib/model'); const scopes = ['read', 'write']; function createModel (db) { @@ -84,7 +85,7 @@ function createModel (db) { return scope.every(s => scopes.includes(s)); } - return { + return Model.from({ getUser, getClient, saveToken, @@ -92,7 +93,7 @@ function createModel (db) { getRefreshToken, revokeToken, verifyScope - }; + }); } module.exports = createModel; diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index d48c1ee0..202c7084 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -6,6 +6,7 @@ const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); const should = require('chai').should(); @@ -45,7 +46,7 @@ describe('AbstractGrantType integration', function() { }); it('should set the `model`', function() { - const model = { async generateAccessToken () {} }; + const model = Model.from({ async generateAccessToken () {} }); const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); grantType.model.should.equal(model); @@ -66,22 +67,22 @@ describe('AbstractGrantType integration', function() { }); it('should support promises', async function() { - const model = { + const model = Model.from({ generateAccessToken: async function() { return 'long-hash-foo-bar'; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); const accessToken = await handler.generateAccessToken(); accessToken.should.equal('long-hash-foo-bar'); }); it('should support non-promises', async function() { - const model = { + const model = Model.from({ generateAccessToken: function() { return 'long-hash-foo-bar'; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); const accessToken = await handler.generateAccessToken(); accessToken.should.equal('long-hash-foo-bar'); @@ -96,22 +97,22 @@ describe('AbstractGrantType integration', function() { }); it('should support promises', async function() { - const model = { + const model = Model.from({ generateRefreshToken: async function() { return 'long-hash-foo-bar'; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); const refreshToken = await handler.generateRefreshToken(); refreshToken.should.equal('long-hash-foo-bar'); }); it('should support non-promises', async function() { - const model = { + const model = Model.from({ generateRefreshToken: function() { return 'long-hash-foo-bar'; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); const refreshToken = await handler.generateRefreshToken(); refreshToken.should.equal('long-hash-foo-bar'); @@ -179,7 +180,7 @@ describe('AbstractGrantType integration', function() { const user = { id: 123 }; const client = { id: 456 }; - const model = { + const model = Model.from({ async validateScope (_user, _client, _scope) { // make sure the model received the correct args _user.should.deep.equal(user); @@ -188,7 +189,7 @@ describe('AbstractGrantType integration', function() { return scope; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); const validated = await handler.validateScope(user, client, scope); validated.should.eql(scope); @@ -201,7 +202,7 @@ describe('AbstractGrantType integration', function() { const returnTypes = [undefined, null, false, 0, '']; for (const type of returnTypes) { - const model = { + const model = Model.from({ async validateScope (_user, _client, _scope) { // make sure the model received the correct args _user.should.deep.equal(user); @@ -210,7 +211,7 @@ describe('AbstractGrantType integration', function() { return type; } - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); try { diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index d705f397..c3ec4c09 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -8,6 +8,7 @@ const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorizati const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const ServerError = require('../../../lib/errors/server-error'); const should = require('chai').should(); @@ -42,9 +43,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function() { try { - const model = { + const model = Model.from({ getAuthorizationCode: function() {} - }; + }); new AuthorizationCodeGrantType({ model: model }); @@ -57,10 +58,10 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() {} - }; + }); new AuthorizationCodeGrantType({ model: model }); @@ -74,11 +75,11 @@ describe('AuthorizationCodeGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); try { @@ -91,14 +92,14 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `client` is invalid (not in code)', async function() { const client = { id: 1234 }; - const model = { + const model = Model.from({ getAuthorizationCode: function(code) { code.should.equal(123456789); return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 123456789 }, headers: {}, method: {}, query: {} }); @@ -112,11 +113,11 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should throw an error if `client` is missing', function() { - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -140,7 +141,7 @@ describe('AuthorizationCodeGrantType integration', function() { user, scope }; - const model = { + const model = Model.from({ getAuthorizationCode: async function (code) { code.should.equal('code-1234'); @@ -178,7 +179,7 @@ describe('AuthorizationCodeGrantType integration', function() { _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return _token; }, - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 'code-1234' }, headers: {}, method: {}, query: {} }); @@ -193,11 +194,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -206,11 +207,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -221,11 +222,11 @@ describe('AuthorizationCodeGrantType integration', function() { describe('getAuthorizationCode()', function() { it('should throw an error if the request body does not contain `code`', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -239,11 +240,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `code` is invalid', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 'øå€£‰' }, headers: {}, method: {}, query: {} }); @@ -258,11 +259,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: async function() {}, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -277,11 +278,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode.client` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return { authorizationCode: 12345 }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -296,13 +297,13 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode.expiresAt` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return { authorizationCode: 12345, client: {}, user: {} }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -317,13 +318,13 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode.user` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -338,13 +339,13 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -360,13 +361,13 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the auth code is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -382,11 +383,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the `redirectUri` is invalid (format)', async function() { const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return authorizationCode; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -406,14 +407,14 @@ describe('AuthorizationCodeGrantType integration', function() { expiresAt: new Date(new Date() * 2), user: {} }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: async function(_code) { _code.should.equal(12345); return authorizationCode; }, revokeAuthorizationCode: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -424,11 +425,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: async function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -438,11 +439,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -453,11 +454,11 @@ describe('AuthorizationCodeGrantType integration', function() { describe('validateRedirectUri()', function() { it('should throw an error if `redirectUri` is missing', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() { return authorizationCode; }, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -473,11 +474,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `redirectUri` is invalid', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, redirect_uri: 'http://bar.foo' }, headers: {}, method: {}, query: {} }); @@ -492,11 +493,11 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('returns undefined and does not throw if `redirectUri` is valid', async function () { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, redirect_uri: 'http://foo.bar' }, headers: {}, method: {}, query: {} }); const value = grantType.validateRedirectUri(request, authorizationCode); @@ -508,14 +509,14 @@ describe('AuthorizationCodeGrantType integration', function() { describe('revokeAuthorizationCode()', function() { it('should revoke the auth code', async function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function(_code) { _code.should.equal(authorizationCode); return true; }, saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const data = await grantType.revokeAuthorizationCode(authorizationCode); @@ -527,14 +528,14 @@ describe('AuthorizationCodeGrantType integration', function() { const returnTypes = [false, null, undefined, 0, '']; for (const type of returnTypes) { - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function(_code) { _code.should.equal(authorizationCode); return type; }, saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); try { @@ -549,22 +550,22 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function() { return true; }, saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: function() { return authorizationCode; }, saveToken: () => should.fail() - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); @@ -573,7 +574,7 @@ describe('AuthorizationCodeGrantType integration', function() { describe('saveToken()', function() { it('should save the token', async function() { const token = { foo: 'bar' }; - const model = { + const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), saveToken: function(_token, _client= 'fallback', _user= 'fallback') { @@ -593,7 +594,7 @@ describe('AuthorizationCodeGrantType integration', function() { _scope.should.eql(['fallback']); return ['foo']; } - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const data = await grantType.saveToken(); data.should.equal(token); @@ -601,11 +602,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() {}, saveToken: async function() { return token; } - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -613,11 +614,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() {}, saveToken: function() { return token; } - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 97d10055..813a2081 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -7,6 +7,7 @@ const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const should = require('chai').should(); @@ -40,9 +41,9 @@ describe('ClientCredentialsGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { - const model = { + const model = Model.from({ getUserFromClient: function() {} - }; + }); new ClientCredentialsGrantType({ model: model }); @@ -56,10 +57,10 @@ describe('ClientCredentialsGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { + const model = Model.from({ getUserFromClient: function() {}, saveToken: function() {} - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); try { @@ -73,10 +74,10 @@ describe('ClientCredentialsGrantType integration', function() { }); it('should throw an error if `client` is missing', async function() { - const model = { + const model = Model.from({ getUserFromClient: function() {}, saveToken: function() {} - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -96,7 +97,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { name: 'foo' }; const scope = ['fooscope']; - const model = { + const model = Model.from({ getUserFromClient: async function(_client) { _client.should.deep.equal(client); return { ...user }; @@ -121,7 +122,7 @@ describe('ClientCredentialsGrantType integration', function() { _scope.should.eql(scope); return 'long-access-token-hash'; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { scope: scope.join(' ') }, headers: {}, method: {}, query: {} }); @@ -131,10 +132,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support promises', function() { const token = {}; - const model = { + const model = Model.from({ getUserFromClient: async function() { return {}; }, saveToken: async function() { return token; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -143,10 +144,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support non-promises', function() { const token = {}; - const model = { + const model = Model.from({ getUserFromClient: function() { return {}; }, saveToken: function() { return token; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -156,10 +157,10 @@ describe('ClientCredentialsGrantType integration', function() { describe('getUserFromClient()', function() { it('should throw an error if `user` is missing', function() { - const model = { + const model = Model.from({ getUserFromClient: function() {}, saveToken: () => should.fail() - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -173,10 +174,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should return a user', function() { const user = { email: 'foo@bar.com' }; - const model = { + const model = Model.from({ getUserFromClient: function() { return user; }, saveToken: () => should.fail() - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -189,10 +190,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support promises', function() { const user = { email: 'foo@bar.com' }; - const model = { + const model = Model.from({ getUserFromClient: async function() { return user; }, saveToken: () => should.fail() - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -201,10 +202,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support non-promises', function() { const user = { email: 'foo@bar.com' }; - const model = { + const model = Model.from({ getUserFromClient: function() {return user; }, saveToken: () => should.fail() - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -215,11 +216,11 @@ describe('ClientCredentialsGrantType integration', function() { describe('saveToken()', function() { it('should save the token', async function() { const token = {}; - const model = { + const model = Model.from({ getUserFromClient: () => should.fail(), saveToken: function() { return token; }, validateScope: function() { return ['foo']; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); const data = await grantType.saveToken(token); data.should.equal(token); @@ -227,10 +228,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support promises', function() { const token = {}; - const model = { + const model = Model.from({ getUserFromClient:() => should.fail(), saveToken: async function() { return token; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -238,10 +239,10 @@ describe('ClientCredentialsGrantType integration', function() { it('should support non-promises', function() { const token = {}; - const model = { + const model = Model.from({ getUserFromClient: () => should.fail(), saveToken: function() { return token; } - }; + }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index ef9b2f16..25da266b 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -8,6 +8,7 @@ const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const should = require('chai').should(); @@ -41,9 +42,9 @@ describe('PasswordGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { - const model = { + const model = Model.from({ getUser: function() {} - }; + }); new PasswordGrantType({ model }); @@ -57,10 +58,10 @@ describe('PasswordGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { @@ -74,10 +75,10 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `client` is missing', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { @@ -100,7 +101,7 @@ describe('PasswordGrantType integration', function() { email: 'foo@example.com' }; - const model = { + const model = Model.from({ getUser: async function(username, password) { username.should.equal('foo'); password.should.equal('bar'); @@ -133,7 +134,7 @@ describe('PasswordGrantType integration', function() { _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); @@ -145,10 +146,10 @@ describe('PasswordGrantType integration', function() { it('should support promises', async function() { const client = { id: 'foobar' }; const token = {}; - const model = { + const model = Model.from({ getUser: async function() { return {}; }, saveToken: async function() { return token; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -159,10 +160,10 @@ describe('PasswordGrantType integration', function() { it('should support non-promises', async function() { const client = { id: 'foobar' }; const token = {}; - const model = { + const model = Model.from({ getUser: function() { return {}; }, saveToken: function() { return token; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -173,10 +174,10 @@ describe('PasswordGrantType integration', function() { describe('getUser()', function() { it('should throw an error if the request body does not contain `username`', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -192,10 +193,10 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if the request body does not contain `password`', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); @@ -211,10 +212,10 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `username` is invalid', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -230,10 +231,10 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `password` is invalid', async function() { - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: () => should.fail() - }; + }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); @@ -249,10 +250,10 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `user` is missing', async function() { - const model = { + const model = Model.from({ getUser: async () => undefined, saveToken: () => should.fail() - }; + }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -269,14 +270,14 @@ describe('PasswordGrantType integration', function() { it('should return a user', async function() { const user = { email: 'foo@bar.com' }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getUser: function(username, password) { username.should.equal('foo'); password.should.equal('bar'); return user; }, saveToken: () => should.fail() - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -287,10 +288,10 @@ describe('PasswordGrantType integration', function() { it('should support promises', function() { const user = { email: 'foo@bar.com' }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getUser: async function() { return user; }, saveToken: () => should.fail() - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -300,10 +301,10 @@ describe('PasswordGrantType integration', function() { it('should support non-promises', function() { const user = { email: 'foo@bar.com' }; const client = { id: 'foobar' }; - const model = { + const model = Model.from({ getUser: function() { return user; }, saveToken: () => should.fail() - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -314,7 +315,7 @@ describe('PasswordGrantType integration', function() { describe('saveToken()', function() { it('should save the token', async function() { const token = {}; - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: async function(_token, _client = 'fallback', _user = 'fallback') { _token.accessToken.should.be.a.sha256(); @@ -330,7 +331,7 @@ describe('PasswordGrantType integration', function() { _scope.should.eql(['fallback']); return ['foo']; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const data = await grantType.saveToken(); @@ -339,10 +340,10 @@ describe('PasswordGrantType integration', function() { it('should support promises', function() { const token = {}; - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: async function() { return token; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -350,10 +351,10 @@ describe('PasswordGrantType integration', function() { it('should support non-promises', function() { const token = {}; - const model = { + const model = Model.from({ getUser: () => should.fail(), saveToken: function() { return token; } - }; + }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 0619fefd..658983aa 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -8,6 +8,7 @@ const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const ServerError = require('../../../lib/errors/server-error'); const should = require('chai').should(); @@ -42,9 +43,9 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `revokeToken()`', function() { try { - const model = { + const model = Model.from({ getRefreshToken: () => should.fail() - }; + }); new RefreshTokenGrantType({ model }); @@ -57,10 +58,10 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail() - }; + }); new RefreshTokenGrantType({ model }); @@ -74,11 +75,11 @@ describe('RefreshTokenGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); try { @@ -92,11 +93,11 @@ describe('RefreshTokenGrantType integration', function() { }); it('should throw an error if `client` is missing', async function() { - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -119,7 +120,7 @@ describe('RefreshTokenGrantType integration', function() { scope: ['read', 'write'], refreshTokenExpiresAt: new Date( new Date() * 2) }; - const model = { + const model = Model.from({ getRefreshToken: async function(_refreshToken) { _refreshToken.should.equal('foobar_refresh'); return token; @@ -149,7 +150,7 @@ describe('RefreshTokenGrantType integration', function() { _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); @@ -159,11 +160,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support promises', function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -172,11 +173,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -187,11 +188,11 @@ describe('RefreshTokenGrantType integration', function() { describe('getRefreshToken()', function() { it('should throw an error if the `refreshToken` parameter is missing from the request body', async function() { const client = {}; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -207,11 +208,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refreshToken` is not found', async function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getRefreshToken: async function() {} , revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); @@ -227,11 +228,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refreshToken.client` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getRefreshToken: async function() { return {}; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -247,13 +248,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refreshToken.user` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -269,13 +270,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -291,13 +292,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refresh_token` contains invalid characters', async function() { const client = {}; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 'øå€£‰' }, headers: {}, method: {}, query: {} }); @@ -313,13 +314,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refresh_token` is missing', async function() { const client = {}; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -336,13 +337,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refresh_token` is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -358,13 +359,13 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refreshTokenExpiresAt` is not a date value', async function() { const client = { id: 123 }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; }, revokeToken: () => should.fail(), saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); @@ -381,7 +382,7 @@ describe('RefreshTokenGrantType integration', function() { it('should return a token', async function() { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: { name: 'foobar' } }; - const model = { + const model = Model.from({ getRefreshToken: async function(_refreshToken) { _refreshToken.should.equal('foobar_refresh'); return token; @@ -399,7 +400,7 @@ describe('RefreshTokenGrantType integration', function() { _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); @@ -410,11 +411,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support promises', function() { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return token; }, revokeToken: async function() {}, saveToken: async function() {} - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -424,11 +425,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - const model = { + const model = Model.from({ getRefreshToken: async function() { return token; }, revokeToken: async function() {}, saveToken: async function() {} - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -438,11 +439,11 @@ describe('RefreshTokenGrantType integration', function() { describe('revokeToken()', function() { it('should throw an error if the `token` is invalid', async function() { - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: async () => {}, saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); try { @@ -456,14 +457,14 @@ describe('RefreshTokenGrantType integration', function() { it('should revoke the token', async function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: async function(_token) { _token.should.deep.equal(token); return token; }, saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const data = await grantType.revokeToken(token); @@ -472,11 +473,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: async function() { return token; }, saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); @@ -484,11 +485,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: function() { return token; }, saveToken: () => should.fail() - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); @@ -500,7 +501,7 @@ describe('RefreshTokenGrantType integration', function() { const user = { name: 'foo' }; const client = { id: 123465 }; const scope = ['foo', 'bar']; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: async function(_token, _client, _user) { @@ -513,7 +514,7 @@ describe('RefreshTokenGrantType integration', function() { _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return { ..._token }; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const data = await grantType.saveToken(user, client, scope); @@ -526,11 +527,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support promises', function() { const token = {}; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: async function() { return token; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -538,11 +539,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = {}; - const model = { + const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), saveToken: function() { return token; } - }; + }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index f7c5323a..c1524f47 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -10,6 +10,7 @@ const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const InsufficientScopeError = require('../../../lib/errors/insufficient-scope-error'); const InvalidTokenError = require('../../../lib/errors/invalid-token-error'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const Response = require('../../../lib/response'); const ServerError = require('../../../lib/errors/server-error'); @@ -78,17 +79,17 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `model`', function() { - const model = { getAccessToken: function() {} }; + const model = Model.from({ getAccessToken: function() {} }); const grantType = new AuthenticateHandler({ model: model }); grantType.model.should.equal(model); }); it('should set the `scope`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const grantType = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, @@ -137,11 +138,11 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', async function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new UnauthorizedRequestError(); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -155,11 +156,11 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `WWW-Authenticate` header if an InvalidRequestError is thrown', function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new InvalidRequestError(); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -172,11 +173,11 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `WWW-Authenticate` header if an InvalidTokenError is thrown', function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new InvalidTokenError(); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -189,11 +190,11 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `WWW-Authenticate` header if an InsufficientScopeError is thrown', function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new InsufficientScopeError(); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -206,11 +207,11 @@ describe('AuthenticateHandler integration', function() { }); it('should throw the error if an oauth error is thrown', function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new AccessDeniedError('Cannot request this access token'); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -224,11 +225,11 @@ describe('AuthenticateHandler integration', function() { }); it('should throw a server error if a non-oauth error is thrown', function() { - const model = { + const model = Model.from({ getAccessToken: function() { throw new Error('Unhandled exception'); } - }; + }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -246,14 +247,14 @@ describe('AuthenticateHandler integration', function() { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; - const model = { + const model = Model.from({ getAccessToken: function() { return accessToken; }, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); const request = new Request({ body: {}, @@ -275,14 +276,14 @@ describe('AuthenticateHandler integration', function() { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; - const model = { + const model = Model.from({ getAccessToken: function() { return accessToken; }, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); const request = new Request({ body: {}, @@ -449,9 +450,9 @@ describe('AuthenticateHandler integration', function() { describe('getAccessToken()', function() { it('should throw an error if `accessToken` is missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() {} - }; + }); const handler = new AuthenticateHandler({ model: model }); return handler.getAccessToken('foo') @@ -463,11 +464,11 @@ describe('AuthenticateHandler integration', function() { }); it('should throw an error if `accessToken.user` is missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() { return {}; } - }; + }); const handler = new AuthenticateHandler({ model: model }); return handler.getAccessToken('foo') @@ -480,11 +481,11 @@ describe('AuthenticateHandler integration', function() { it('should return an access token', function() { const accessToken = { user: {} }; - const model = { + const model = Model.from({ getAccessToken: function() { return accessToken; } - }; + }); const handler = new AuthenticateHandler({ model: model }); return handler.getAccessToken('foo') @@ -495,22 +496,22 @@ describe('AuthenticateHandler integration', function() { }); it('should support promises', function() { - const model = { + const model = Model.from({ getAccessToken: async function() { return { user: {} }; } - }; + }); const handler = new AuthenticateHandler({ model: model }); handler.getAccessToken('foo').should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() { return { user: {} }; } - }; + }); const handler = new AuthenticateHandler({ model: model }); handler.getAccessToken('foo').should.be.an.instanceOf(Promise); @@ -545,12 +546,12 @@ describe('AuthenticateHandler integration', function() { describe('verifyScope()', function() { it('should throw an error if `scope` is insufficient (deprecated)', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return false; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); return handler.verifyScope(['foo']) @@ -562,12 +563,12 @@ describe('AuthenticateHandler integration', function() { }); it('should throw an error if `scope` is insufficient', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return false; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); return handler.verifyScope(['foo']) @@ -579,48 +580,48 @@ describe('AuthenticateHandler integration', function() { }); it('should support promises (deprecated)', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); }); it('should support promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); }); it('should support non-promises (deprecated)', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() { return true; } - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); @@ -629,10 +630,10 @@ describe('AuthenticateHandler integration', function() { describe('updateResponse()', function() { it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model }); const response = new Response({ body: {}, headers: {} }); @@ -642,10 +643,10 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: 'foo bar' }); const response = new Response({ body: {}, headers: {} }); @@ -655,10 +656,10 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); @@ -668,10 +669,10 @@ describe('AuthenticateHandler integration', function() { }); it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model }); const response = new Response({ body: {}, headers: {} }); @@ -681,10 +682,10 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `X-Authorized-OAuth-Scopes` header (deprecated)', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: 'foo bar' }); const response = new Response({ body: {}, headers: {} }); @@ -694,10 +695,10 @@ describe('AuthenticateHandler integration', function() { }); it('should set the `X-Authorized-OAuth-Scopes` header', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: function() {} - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 8bc3ae09..fb6904d4 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -13,6 +13,7 @@ const InvalidClientError = require('../../../lib/errors/invalid-client-error'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); const UnsupportedResponseTypeError = require('../../../lib/errors/unsupported-response-type-error'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const Response = require('../../../lib/response'); const ServerError = require('../../../lib/errors/server-error'); @@ -21,12 +22,12 @@ const should = require('chai').should(); const url = require('url'); const createModel = (model = {}) => { - return { + return Model.from({ getAccessToken: () => should.fail(), getClient: () => should.fail(), saveAuthorizationCode: () => should.fail(), ...model - }; + }); }; /** @@ -76,10 +77,10 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if the model does not implement `getAccessToken()`', function() { - const model = { + const model = Model.from({ getClient: () => should.fail(), saveAuthorizationCode: () => should.fail() - }; + }); try { new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); @@ -404,7 +405,7 @@ describe('AuthorizeHandler integration', function() { it('should redirect to a successful response if `model.validateScope` is not defined', async function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { + const model = Model.from({ getAccessToken: function() { return { client: client, @@ -418,7 +419,7 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() { return { authorizationCode: 'fooobar-long-authzcode-?', client }; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { @@ -449,7 +450,7 @@ describe('AuthorizeHandler integration', function() { it('should redirect to an error response if `scope` is insufficient (validateScope)', async function() { const client = { id: 12345, grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { + const model = Model.from({ getAccessToken: async function() { return { client: client, @@ -467,7 +468,7 @@ describe('AuthorizeHandler integration', function() { _scope.should.eql(['read']); return false; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { @@ -543,7 +544,7 @@ describe('AuthorizeHandler integration', function() { }); it('should redirect to an error response if `response_type` is invalid', async function() { - const model = { + const model = Model.from({ getAccessToken: async function() { return { user: {}, @@ -554,7 +555,7 @@ describe('AuthorizeHandler integration', function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: () => should.fail() // should fail before call - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { @@ -587,7 +588,7 @@ describe('AuthorizeHandler integration', function() { it('should return the `code` if successful', async function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { + const model = Model.from({ getAccessToken: async function() { return { client: client, @@ -602,7 +603,7 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: async function(code) { return { authorizationCode: code.authorizationCode, client: client }; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { @@ -643,7 +644,7 @@ describe('AuthorizeHandler integration', function() { scope, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; - const model = { + const model = Model.from({ getClient: async function (clientId, clientSecret) { clientId.should.equal(client.id); (clientSecret === null).should.equal(true); @@ -677,7 +678,7 @@ describe('AuthorizeHandler integration', function() { _client.should.deep.equal(client); return { ...code, client, user }; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ @@ -715,7 +716,7 @@ describe('AuthorizeHandler integration', function() { } }; const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { + const model = Model.from({ getAccessToken: async function() { return { client: client, @@ -730,7 +731,7 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: async function(code) { return { authorizationCode: code.authorizationCode, client: client }; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model, authenticateHandler }); const request = new Request({ body: { @@ -757,11 +758,11 @@ describe('AuthorizeHandler integration', function() { describe('generateAuthorizationCode()', function() { it('should return an auth code', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.generateAuthorizationCode() @@ -772,28 +773,28 @@ describe('AuthorizeHandler integration', function() { }); it('should support promises', function() { - const model = { + const model = Model.from({ generateAuthorizationCode: async function() { return {}; }, getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { - const model = { + const model = Model.from({ generateAuthorizationCode: function() { return {}; }, getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); @@ -802,11 +803,11 @@ describe('AuthorizeHandler integration', function() { describe('getAuthorizationCodeLifetime()', function() { it('should return a date', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); @@ -815,11 +816,11 @@ describe('AuthorizeHandler integration', function() { describe('validateRedirectUri()', function() { it('should support empty method', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); @@ -827,14 +828,14 @@ describe('AuthorizeHandler integration', function() { }); it('should support promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {}, validateRedirectUri: async function() { return true; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); @@ -842,14 +843,14 @@ describe('AuthorizeHandler integration', function() { }); it('should support non-promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {}, validateRedirectUri: function() { return true; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); @@ -859,11 +860,11 @@ describe('AuthorizeHandler integration', function() { describe('getClient()', function() { it('should throw an error if `client_id` is missing', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -878,11 +879,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client_id` is invalid', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 'øå€£‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -897,11 +898,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client.redirectUri` is invalid', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -916,11 +917,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client` is missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -933,13 +934,13 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client.grants` is missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return {}; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -952,13 +953,13 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client` is unauthorized', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return { grants: [] }; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -971,11 +972,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client.redirectUri` is missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return { grants: ['authorization_code'] }; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); @@ -988,13 +989,13 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return { grants: ['authorization_code'], redirectUris: ['https://example.com'] }; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); @@ -1007,13 +1008,13 @@ describe('AuthorizeHandler integration', function() { }); it('should support promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, @@ -1026,13 +1027,13 @@ describe('AuthorizeHandler integration', function() { }); it('should support non-promises', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, @@ -1047,13 +1048,13 @@ describe('AuthorizeHandler integration', function() { describe('with `client_id` in the request query', function() { it('should return a client', function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() { return client; }, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); @@ -1068,11 +1069,11 @@ describe('AuthorizeHandler integration', function() { describe('getScope()', function() { it('should throw an error if `scope` is invalid', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); @@ -1088,11 +1089,11 @@ describe('AuthorizeHandler integration', function() { describe('with `scope` in the request body', function() { it('should return the scope', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); @@ -1102,11 +1103,11 @@ describe('AuthorizeHandler integration', function() { describe('with `scope` in the request query', function() { it('should return the scope', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); @@ -1117,11 +1118,11 @@ describe('AuthorizeHandler integration', function() { describe('getState()', function() { it('should throw an error if `allowEmptyState` is false and `state` is missing', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -1136,11 +1137,11 @@ describe('AuthorizeHandler integration', function() { }); it('should allow missing `state` if `allowEmptyState` is valid', function () { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const state = handler.getState(request); @@ -1148,11 +1149,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `state` is invalid', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'øå€£‰' } }); @@ -1168,11 +1169,11 @@ describe('AuthorizeHandler integration', function() { describe('with `state` in the request body', function() { it('should return the state', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -1182,11 +1183,11 @@ describe('AuthorizeHandler integration', function() { describe('with `state` in the request query', function() { it('should return the state', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); @@ -1198,10 +1199,10 @@ describe('AuthorizeHandler integration', function() { describe('getUser()', function() { it('should throw an error if `user` is missing', function() { const authenticateHandler = { handle: function() {} }; - const model = { + const model = Model.from({ getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const response = new Response(); @@ -1216,7 +1217,7 @@ describe('AuthorizeHandler integration', function() { it('should return a user', function() { const user = {}; - const model = { + const model = Model.from({ getAccessToken: function() { return { user: user, @@ -1225,7 +1226,7 @@ describe('AuthorizeHandler integration', function() { }, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -1241,13 +1242,13 @@ describe('AuthorizeHandler integration', function() { describe('saveAuthorizationCode()', function() { it('should return an auth code', function() { const authorizationCode = {}; - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') @@ -1258,26 +1259,26 @@ describe('AuthorizeHandler integration', function() { }); it('should support promises when calling `model.saveAuthorizationCode()`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: async function() { return {}; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); it('should support non-promises when calling `model.saveAuthorizationCode()`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() { return {}; } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); @@ -1286,11 +1287,11 @@ describe('AuthorizeHandler integration', function() { describe('getResponseType()', function() { it('should throw an error if `response_type` is missing', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -1305,11 +1306,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `response_type` is not `code`', async function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -1325,11 +1326,11 @@ describe('AuthorizeHandler integration', function() { describe('with `response_type` in the request body', function() { it('should return a response type', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); const ResponseType = handler.getResponseType(request); @@ -1340,11 +1341,11 @@ describe('AuthorizeHandler integration', function() { describe('with `response_type` in the request query', function() { it('should return a response type', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); const ResponseType = handler.getResponseType(request); @@ -1356,11 +1357,11 @@ describe('AuthorizeHandler integration', function() { describe('buildSuccessRedirectUri()', function() { it('should return a redirect uri', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const responseType = new CodeResponseType(12345); const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); @@ -1372,11 +1373,11 @@ describe('AuthorizeHandler integration', function() { describe('buildErrorRedirectUri()', function() { it('should set `error_description` if available', function() { const error = new InvalidClientError('foo bar'); - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); @@ -1385,11 +1386,11 @@ describe('AuthorizeHandler integration', function() { it('should return a redirect uri', function() { const error = new InvalidClientError(); - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); @@ -1399,11 +1400,11 @@ describe('AuthorizeHandler integration', function() { describe('updateResponse()', function() { it('should set the `location` header', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const response = new Response({ body: {}, headers: {} }); const uri = url.parse('http://example.com/cb'); @@ -1416,11 +1417,11 @@ describe('AuthorizeHandler integration', function() { describe('getCodeChallengeMethod()', function() { it('should get code challenge method', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} }); @@ -1429,11 +1430,11 @@ describe('AuthorizeHandler integration', function() { }); it('should throw if the code challenge method is not supported', async function () { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'foo'}, headers: {}, method: {}, query: {} }); @@ -1449,11 +1450,11 @@ describe('AuthorizeHandler integration', function() { }); it('should get default code challenge method plain if missing', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -1464,11 +1465,11 @@ describe('AuthorizeHandler integration', function() { describe('getCodeChallenge()', function() { it('should get code challenge', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 53c9c40f..46f3ab19 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -11,6 +11,7 @@ const InvalidClientError = require('../../../lib/errors/invalid-client-error'); const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); +const Model = require('../../../lib/model'); const Request = require('../../../lib/request'); const Response = require('../../../lib/response'); const ServerError = require('../../../lib/errors/server-error'); @@ -74,10 +75,10 @@ describe('TokenHandler integration', function() { it('should set the `accessTokenLifetime`', function() { const accessTokenLifetime = {}; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: accessTokenLifetime, model: model, refreshTokenLifetime: 120 }); handler.accessTokenLifetime.should.equal(accessTokenLifetime); @@ -85,10 +86,10 @@ describe('TokenHandler integration', function() { it('should set the `alwaysIssueNewRefreshToken`', function() { const alwaysIssueNewRefreshToken = true; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); @@ -96,20 +97,20 @@ describe('TokenHandler integration', function() { it('should set the `alwaysIssueNewRefreshToken` to false', function() { const alwaysIssueNewRefreshToken = false; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); }); it('should return the default `alwaysIssueNewRefreshToken` value', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120 }); handler.alwaysIssueNewRefreshToken.should.equal(true); @@ -117,19 +118,19 @@ describe('TokenHandler integration', function() { it('should set the `extendedGrantTypes`', function() { const extendedGrantTypes = { foo: 'bar' }; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, extendedGrantTypes: extendedGrantTypes, model: model, refreshTokenLifetime: 120 }); handler.grantTypes.should.deep.include(extendedGrantTypes); }); it('should set the `model`', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); handler.model.should.equal(model); @@ -137,10 +138,10 @@ describe('TokenHandler integration', function() { it('should set the `refreshTokenLifetime`', function() { const refreshTokenLifetime = {}; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: refreshTokenLifetime }); handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); @@ -149,10 +150,10 @@ describe('TokenHandler integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); try { @@ -166,10 +167,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `response` is missing', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -184,10 +185,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if the method is not `POST`', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -201,10 +202,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -218,10 +219,10 @@ describe('TokenHandler integration', function() { }); it('should throw the error if an oauth error is thrown', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -235,13 +236,13 @@ describe('TokenHandler integration', function() { }); it('should throw a server error if a non-oauth error is thrown', function() { - const model = { + const model = Model.from({ getClient: function() { throw new Error('Unhandled exception'); }, getUser: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -267,13 +268,13 @@ describe('TokenHandler integration', function() { }); it('should update the response if an error is thrown', function() { - const model = { + const model = Model.from({ getClient: function() { throw new Error('Unhandled exception'); }, getUser: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -299,12 +300,12 @@ describe('TokenHandler integration', function() { it('should return a bearer token if successful', function() { const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['foobar'], user: {} }; - const model = { + const model = Model.from({ getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['baz']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -330,12 +331,12 @@ describe('TokenHandler integration', function() { it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function() { const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['baz'], user: {}, foo: 'bar' }; - const model = { + const model = Model.from({ getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['baz']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -365,12 +366,12 @@ describe('TokenHandler integration', function() { it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function() { const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['baz'], user: {}, foo: 'bar' }; - const model = { + const model = Model.from({ getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['baz']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, allowExtendedTokenAttributes: true }); const request = new Request({ body: { @@ -402,10 +403,10 @@ describe('TokenHandler integration', function() { describe('getClient()', function() { it('should throw an error if `clientId` is invalid', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); @@ -420,10 +421,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `clientSecret` is invalid', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); @@ -438,10 +439,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `client` is missing', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -454,10 +455,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `client.grants` is missing', function() { - const model = { + const model = Model.from({ getClient: function() { return {}; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -470,10 +471,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `client.grants` is invalid', function() { - const model = { + const model = Model.from({ getClient: function() { return { grants: 'foobar' }; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -486,10 +487,10 @@ describe('TokenHandler integration', function() { }); it('should throw a 401 error if the client is invalid and the request contains an authorization header', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, @@ -512,10 +513,10 @@ describe('TokenHandler integration', function() { it('should return a client', function() { const client = { id: 12345, grants: [] }; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -530,10 +531,10 @@ describe('TokenHandler integration', function() { it('should return a client ', function() { const client = { id: 12345, grants: [] }; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, @@ -557,10 +558,10 @@ describe('TokenHandler integration', function() { it('should return a client ', function() { const client = { id: 12345, grants: [] }; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, @@ -586,10 +587,10 @@ describe('TokenHandler integration', function() { }); it('should support promises', function() { - const model = { + const model = Model.from({ getClient: async function() { return { grants: [] }; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -597,10 +598,10 @@ describe('TokenHandler integration', function() { }); it('should support non-promises', function() { - const model = { + const model = Model.from({ getClient: function() { return { grants: [] }; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -610,10 +611,10 @@ describe('TokenHandler integration', function() { describe('getClientCredentials()', function() { it('should throw an error if `client_id` is missing', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); @@ -628,10 +629,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `client_secret` is missing', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); @@ -647,10 +648,10 @@ describe('TokenHandler integration', function() { describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function() { it('should return a client', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, requireClientAuthentication: { password: false} }); const request = new Request({ body: { client_id: 'foo', grant_type: 'password' }, headers: {}, method: {}, query: {} }); const credentials = handler.getClientCredentials(request); @@ -661,10 +662,10 @@ describe('TokenHandler integration', function() { describe('with `client_id` and `client_secret` in the request header as basic auth', function() { it('should return a client', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, @@ -682,10 +683,10 @@ describe('TokenHandler integration', function() { describe('with `client_id` and `client_secret` in the request body', function() { it('should return a client', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); const credentials = handler.getClientCredentials(request); @@ -697,10 +698,10 @@ describe('TokenHandler integration', function() { describe('handleGrantType()', function() { it('should throw an error if `grant_type` is missing', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -715,10 +716,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `grant_type` is invalid', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, query: {} }); @@ -733,10 +734,10 @@ describe('TokenHandler integration', function() { }); it('should throw an error if `grant_type` is unsupported', async function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, query: {} }); @@ -752,10 +753,10 @@ describe('TokenHandler integration', function() { it('should throw an error if `grant_type` is unauthorized', async function() { const client = { grants: ['client_credentials'] }; - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); @@ -771,11 +772,11 @@ describe('TokenHandler integration', function() { it('should throw an invalid grant error if a non-oauth error is thrown', function() { const client = { grants: ['password'] }; - const model = { + const model = Model.from({ getClient: function(clientId, password) { return client; }, getUser: function(uid, pwd) {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'password', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -791,13 +792,13 @@ describe('TokenHandler integration', function() { it('should return a token', function() { const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -830,13 +831,13 @@ describe('TokenHandler integration', function() { }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -868,13 +869,13 @@ describe('TokenHandler integration', function() { }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -906,13 +907,13 @@ describe('TokenHandler integration', function() { }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -945,13 +946,13 @@ describe('TokenHandler integration', function() { }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -980,13 +981,13 @@ describe('TokenHandler integration', function() { }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -1012,12 +1013,12 @@ describe('TokenHandler integration', function() { it('should return a token', function() { const client = { grants: ['client_credentials'] }; const token = {}; - const model = { + const model = Model.from({ getClient: function() {}, getUserFromClient: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['foo']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -1041,12 +1042,12 @@ describe('TokenHandler integration', function() { it('should return a token', function() { const client = { grants: ['password'] }; const token = {}; - const model = { + const model = Model.from({ getClient: function() {}, getUser: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['baz']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -1074,12 +1075,12 @@ describe('TokenHandler integration', function() { it('should return a token', function() { const client = { grants: ['refresh_token'] }; const token = { accessToken: 'foo', client: {}, user: {} }; - const model = { + const model = Model.from({ getClient: function() {}, getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; }, saveToken: function() { return token; }, revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -1103,12 +1104,12 @@ describe('TokenHandler integration', function() { it('should return a token', function() { const client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; const token = {}; - const model = { + const model = Model.from({ getClient: function() {}, getUser: function() { return {}; }, saveToken: function() { return token; }, validateScope: function() { return ['foo']; } - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType } }); const request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -1124,10 +1125,10 @@ describe('TokenHandler integration', function() { describe('getAccessTokenLifetime()', function() { it('should return the client access token lifetime', function() { const client = { accessTokenLifetime: 60 }; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); handler.getAccessTokenLifetime(client).should.equal(60); @@ -1135,10 +1136,10 @@ describe('TokenHandler integration', function() { it('should return the default access token lifetime', function() { const client = {}; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); handler.getAccessTokenLifetime(client).should.equal(120); @@ -1148,10 +1149,10 @@ describe('TokenHandler integration', function() { describe('getRefreshTokenLifetime()', function() { it('should return the client access token lifetime', function() { const client = { refreshTokenLifetime: 60 }; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); handler.getRefreshTokenLifetime(client).should.equal(60); @@ -1159,10 +1160,10 @@ describe('TokenHandler integration', function() { it('should return the default access token lifetime', function() { const client = {}; - const model = { + const model = Model.from({ getClient: function() { return client; }, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); handler.getRefreshTokenLifetime(client).should.equal(120); @@ -1171,10 +1172,10 @@ describe('TokenHandler integration', function() { describe('getTokenType()', function() { it('should return a token type', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: ['foobar'] }); tokenType.should.deep.include({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: ['foobar'] }); @@ -1183,10 +1184,10 @@ describe('TokenHandler integration', function() { describe('updateSuccessResponse()', function() { it('should set the `body`', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1197,10 +1198,10 @@ describe('TokenHandler integration', function() { }); it('should set the `Cache-Control` header', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1211,10 +1212,10 @@ describe('TokenHandler integration', function() { }); it('should set the `Pragma` header', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1228,10 +1229,10 @@ describe('TokenHandler integration', function() { describe('updateErrorResponse()', function() { it('should set the `body`', function() { const error = new AccessDeniedError('Cannot request a token'); - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const response = new Response({ body: {}, headers: {} }); @@ -1243,10 +1244,10 @@ describe('TokenHandler integration', function() { it('should set the `status`', function() { const error = new AccessDeniedError('Cannot request a token'); - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 7732bdc2..f16aecce 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -5,6 +5,7 @@ */ const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); +const Model = require('../../lib/model'); const Request = require('../../lib/request'); const Response = require('../../lib/response'); const Server = require('../../lib/server'); @@ -30,7 +31,7 @@ describe('Server integration', function() { }); it('should set the `model`', function() { - const model = {}; + const model = Model.from({}); const server = new Server({ model: model }); server.options.model.should.equal(model); @@ -39,14 +40,14 @@ describe('Server integration', function() { describe('authenticate()', function() { it('should set the default `options`', async function() { - const model = { + const model = Model.from({ getAccessToken: function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -62,14 +63,14 @@ describe('Server integration', function() { }); it('should return a promise', function() { - const model = { + const model = Model.from({ getAccessToken: async function(token) { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -81,7 +82,7 @@ describe('Server integration', function() { describe('authorize()', function() { it('should set the default `options`', async function() { - const model = { + const model = Model.from({ getAccessToken: function() { return { user: {}, @@ -94,7 +95,7 @@ describe('Server integration', function() { saveAuthorizationCode: function() { return { authorizationCode: 123 }; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); const response = new Response({ body: {}, headers: {} }); @@ -109,7 +110,7 @@ describe('Server integration', function() { }); it('should return a promise', function() { - const model = { + const model = Model.from({ getAccessToken: function() { return { user: {}, @@ -122,7 +123,7 @@ describe('Server integration', function() { saveAuthorizationCode: function() { return { authorizationCode: 123 }; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); const response = new Response({ body: {}, headers: {} }); @@ -134,7 +135,7 @@ describe('Server integration', function() { describe('token()', function() { it('should set the default `options`', async function() { - const model = { + const model = Model.from({ getClient: function() { return { grants: ['password'] }; }, @@ -145,7 +146,7 @@ describe('Server integration', function() { return { accessToken: 1234, client: {}, user: {} }; }, validateScope: function() { return ['foo']; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass', scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -160,7 +161,7 @@ describe('Server integration', function() { }); it('should return a promise', function() { - const model = { + const model = Model.from({ getClient: function() { return { grants: ['password'] }; }, @@ -170,7 +171,7 @@ describe('Server integration', function() { saveToken: function() { return { accessToken: 1234, client: {}, user: {} }; } - }; + }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); const response = new Response({ body: {}, headers: {} }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index f6cb13a4..236574ce 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -5,6 +5,7 @@ */ const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); @@ -15,9 +16,9 @@ const should = require('chai').should(); describe('AbstractGrantType', function() { describe('generateAccessToken()', function() { it('should call `model.generateAccessToken()`', function() { - const model = { + const model = Model.from({ generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); return handler.generateAccessToken() @@ -31,9 +32,9 @@ describe('AbstractGrantType', function() { describe('generateRefreshToken()', function() { it('should call `model.generateRefreshToken()`', function() { - const model = { + const model = Model.from({ generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), user: {} }) - }; + }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); return handler.generateRefreshToken() diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index 3ffe46ad..dd22b0f5 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -8,6 +8,7 @@ const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorizati const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); const ServerError = require('../../../lib/errors/server-error'); const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); const stringUtil = require('../../../lib/utils/string-util'); @@ -20,11 +21,11 @@ const crypto = require('crypto'); describe('AuthorizationCodeGrantType', function() { describe('getAuthorizationCode()', function() { it('should call `model.getAuthorizationCode()`', function() { - const model = { + const model = Model.from({ getAuthorizationCode: sinon.stub().returns({ authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); const client = {}; @@ -42,11 +43,11 @@ describe('AuthorizationCodeGrantType', function() { describe('revokeAuthorizationCode()', function() { it('should call `model.revokeAuthorizationCode()`', function() { - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: sinon.stub().returns(true), saveToken: function() {} - }; + }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); const authorizationCode = {}; @@ -65,11 +66,11 @@ describe('AuthorizationCodeGrantType', function() { it('should call `model.saveToken()`', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getAuthorizationCode: function() {}, revokeAuthorizationCode: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); sinon.stub(handler, 'validateScope').returns(['foobiz']); @@ -103,11 +104,11 @@ describe('AuthorizationCodeGrantType', function() { codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', isPublic: true }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, code_verifier: 'foo' }, headers: {}, method: {}, query: {} }); @@ -130,11 +131,11 @@ describe('AuthorizationCodeGrantType', function() { codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', isPublic: true }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} }); @@ -158,11 +159,11 @@ describe('AuthorizationCodeGrantType', function() { }; // fixme: The isPublic option is not used, as a result any client which allows authorization_code grant also accepts PKCE requests. const client = { id: 'foobar', isPublic: true }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, code_verifier: 'foo' }, headers: {}, method: {}, query: {} }); @@ -185,11 +186,11 @@ describe('AuthorizationCodeGrantType', function() { codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', isPublic: true }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} }); @@ -211,11 +212,11 @@ describe('AuthorizationCodeGrantType', function() { codeChallenge: codeVerifier }; const client = { id: 'foobar', isPublic: true }; - const model = { + const model = Model.from({ getAuthorizationCode: function() { return authorizationCode; }, revokeAuthorizationCode: function() {}, saveToken: function() {} - }; + }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} }); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index 5e012b43..17d386cf 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -5,6 +5,7 @@ */ const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); @@ -15,10 +16,10 @@ const should = require('chai').should(); describe('ClientCredentialsGrantType', function() { describe('getUserFromClient()', function() { it('should call `model.getUserFromClient()`', function() { - const model = { + const model = Model.from({ getUserFromClient: sinon.stub().returns(true), saveToken: function() {} - }; + }); const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const client = {}; @@ -37,10 +38,10 @@ describe('ClientCredentialsGrantType', function() { it('should call `model.saveToken()`', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getUserFromClient: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); sinon.stub(handler, 'validateScope').returns(['foobar']); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index 63f43933..398b2929 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -6,6 +6,7 @@ const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); @@ -16,10 +17,10 @@ const should = require('chai').should(); describe('PasswordGrantType', function() { describe('getUser()', function() { it('should call `model.getUser()`', function() { - const model = { + const model = Model.from({ getUser: sinon.stub().returns(true), saveToken: function() {} - }; + }); const client = { id: 'foobar' }; const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -40,10 +41,10 @@ describe('PasswordGrantType', function() { it('should call `model.saveToken()`', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getUser: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); sinon.stub(handler, 'validateScope').returns(['foobar']); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index 8d2faee6..ed0c9e2a 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -6,6 +6,7 @@ const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); @@ -17,11 +18,11 @@ describe('RefreshTokenGrantType', function() { describe('handle()', function() { it('should revoke the previous token', function() { const token = { accessToken: 'foo', client: {}, user: {} }; - const model = { + const model = Model.from({ getRefreshToken: function() { return token; }, saveToken: function() { return { accessToken: 'bar', client: {}, user: {} }; }, revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }) - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); const client = {}; @@ -39,11 +40,11 @@ describe('RefreshTokenGrantType', function() { describe('getRefreshToken()', function() { it('should call `model.getRefreshToken()`', function() { - const model = { + const model = Model.from({ getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), saveToken: function() {}, revokeToken: function() {} - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); const client = {}; @@ -61,11 +62,11 @@ describe('RefreshTokenGrantType', function() { describe('revokeToken()', function() { it('should call `model.revokeToken()`', function() { - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); const token = {}; @@ -80,11 +81,11 @@ describe('RefreshTokenGrantType', function() { }); it('should not call `model.revokeToken()`', function() { - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); const token = {}; @@ -96,11 +97,11 @@ describe('RefreshTokenGrantType', function() { }); it('should not call `model.revokeToken()`', function() { - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true }); const token = {}; @@ -119,11 +120,11 @@ describe('RefreshTokenGrantType', function() { it('should call `model.saveToken()`', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); sinon.stub(handler, 'generateAccessToken').returns('foo'); @@ -146,11 +147,11 @@ describe('RefreshTokenGrantType', function() { it('should call `model.saveToken()` without refresh token', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); sinon.stub(handler, 'generateAccessToken').returns('foo'); @@ -173,11 +174,11 @@ describe('RefreshTokenGrantType', function() { it('should call `model.saveToken()` with refresh token', function() { const client = {}; const user = {}; - const model = { + const model = Model.from({ getRefreshToken: function() {}, revokeToken: function() {}, saveToken: sinon.stub().returns(true) - }; + }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true}); sinon.stub(handler, 'generateAccessToken').returns('foo'); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 6919718f..e5384e01 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -7,6 +7,7 @@ const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); const ServerError = require('../../../lib/errors/server-error'); @@ -107,9 +108,9 @@ describe('AuthenticateHandler', function() { describe('getAccessToken()', function() { it('should call `model.getAccessToken()`', function() { - const model = { + const model = Model.from({ getAccessToken: sinon.stub().returns({ user: {} }) - }; + }); const handler = new AuthenticateHandler({ model: model }); return handler.getAccessToken('foo') @@ -125,9 +126,9 @@ describe('AuthenticateHandler', function() { describe('validateAccessToken()', function() { it('should fail if token has no valid `accessTokenExpiresAt` date', function() { - const model = { + const model = Model.from({ getAccessToken: function() {} - }; + }); const handler = new AuthenticateHandler({ model: model }); let failed = false; @@ -144,9 +145,9 @@ describe('AuthenticateHandler', function() { }); it('should succeed if token has valid `accessTokenExpiresAt` date', function() { - const model = { + const model = Model.from({ getAccessToken: function() {} - }; + }); const handler = new AuthenticateHandler({ model: model }); try { handler.validateAccessToken({ @@ -162,10 +163,10 @@ describe('AuthenticateHandler', function() { describe('verifyScope()', function() { it('should call `model.getAccessToken()` if scope is defined', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, verifyScope: sinon.stub().returns(true) - }; + }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); return handler.verifyScope(['foo']) diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 078f82f8..b539e480 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -7,6 +7,7 @@ const AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); const Request = require('../../../lib/request'); const Response = require('../../../lib/response'); +const Model = require('../../../lib/model'); const sinon = require('sinon'); const should = require('chai').should(); @@ -17,12 +18,12 @@ const should = require('chai').should(); describe('AuthorizeHandler', function() { describe('generateAuthorizationCode()', function() { it('should call `model.generateAuthorizationCode()`', function() { - const model = { + const model = Model.from({ generateAuthorizationCode: sinon.stub().returns({}), getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); return handler.generateAuthorizationCode() @@ -36,11 +37,11 @@ describe('AuthorizeHandler', function() { describe('getClient()', function() { it('should call `model.getClient()`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }), saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -58,10 +59,10 @@ describe('AuthorizeHandler', function() { describe('getUser()', function() { it('should call `authenticateHandler.getUser()`', function() { const authenticateHandler = { handle: sinon.stub().returns(Promise.resolve({})) }; - const model = { + const model = Model.from({ getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const response = new Response(); @@ -79,11 +80,11 @@ describe('AuthorizeHandler', function() { describe('saveAuthorizationCode()', function() { it('should call `model.saveAuthorizationCode()`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: sinon.stub().returns({}) - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz') @@ -99,11 +100,11 @@ describe('AuthorizeHandler', function() { }); it('should call `model.saveAuthorizationCode()` with code challenge', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: sinon.stub().returns({}) - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') @@ -123,12 +124,12 @@ describe('AuthorizeHandler', function() { it('should call `model.validateRedirectUri()`', function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/cb/2'; - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: sinon.stub().returns(client), saveAuthorizationCode: function() {}, validateRedirectUri: sinon.stub().returns(true) - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); @@ -151,14 +152,14 @@ describe('AuthorizeHandler', function() { it('should be successful validation', function () { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/cb'; - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: sinon.stub().returns(client), saveAuthorizationCode: function() {}, validateRedirectUri: function (redirectUri, client) { return client.redirectUris.includes(redirectUri); } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); @@ -172,14 +173,14 @@ describe('AuthorizeHandler', function() { it('should be unsuccessful validation', function () { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/callback'; - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: sinon.stub().returns(client), saveAuthorizationCode: function() {}, validateRedirectUri: function (redirectUri, client) { return client.redirectUris.includes(redirectUri); } - }; + }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 8fe258b9..a16be32d 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -5,6 +5,7 @@ */ const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); const TokenHandler = require('../../../lib/handlers/token-handler'); const sinon = require('sinon'); const should = require('chai').should(); @@ -16,10 +17,10 @@ const should = require('chai').should(); describe('TokenHandler', function() { describe('getClient()', function() { it('should call `model.getClient()`', function() { - const model = { + const model = Model.from({ getClient: sinon.stub().returns({ grants: ['password'] }), saveToken: function() {} - }; + }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); diff --git a/test/unit/models/model_wrapper_test.js b/test/unit/models/model_wrapper_test.js new file mode 100644 index 00000000..75b8ebb4 --- /dev/null +++ b/test/unit/models/model_wrapper_test.js @@ -0,0 +1,33 @@ +const Model = require('../../../lib/model'); +const {expect} = require('chai'); + +describe('ModelWrapper', () => { + const expectThrows = async (fn) => { + try { + await fn(); + expect.fail(); + } catch (e) { + expect(e.message).to.include('not implemented'); + } + }; + it('throws on all functions when used via constructor', async () => { + const m = new Model(); + await expectThrows(() => m.generateAccessToken()); + await expectThrows(() => m.generateAuthorizationCode()); + await expectThrows(() => m.generateRefreshToken()); + await expectThrows(() => m.getAccessToken()); + await expectThrows(() => m.getAuthorizationCode()); + await expectThrows(() => m.getClient()); + await expectThrows(() => m.getRefreshToken()); + await expectThrows(() => m.getUser()); + await expectThrows(() => m.getUserFromClient()); + await expectThrows(() => m.revokeAuthorizationCode()); + await expectThrows(() => m.revokeToken()); + await expectThrows(() => m.saveAuthorizationCode()); + await expectThrows(() => m.saveToken()); + await expectThrows(() => m.verifyScope()); + await expectThrows(() => m.validateRedirectUri()); + await expectThrows(() => m.validateScope()); + + }); +}); \ No newline at end of file diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 3f2688df..338a7f9d 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -5,7 +5,7 @@ const should = require('chai').should(); * Test `Server`. */ -describe('Model', function() { +describe('TokenModel', function() { describe('constructor()', function() { it('throws, if data is empty', function () { try { diff --git a/test/unit/server_test.js b/test/unit/server_test.js index fd7bd391..f8caa2bc 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -8,6 +8,7 @@ const AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); const AuthorizeHandler = require('../../lib/handlers/authorize-handler'); const Server = require('../../lib/server'); const TokenHandler = require('../../lib/handlers/token-handler'); +const Model = require('../../lib/model'); const sinon = require('sinon'); /** @@ -17,9 +18,9 @@ const sinon = require('sinon'); describe('Server', function() { describe('authenticate()', function() { it('should call `handle`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {} - }; + }); const server = new Server({ model: model }); sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); @@ -34,11 +35,11 @@ describe('Server', function() { describe('authorize()', function() { it('should call `handle`', function() { - const model = { + const model = Model.from({ getAccessToken: function() {}, getClient: function() {}, saveAuthorizationCode: function() {} - }; + }); const server = new Server({ model: model }); sinon.stub(AuthorizeHandler.prototype, 'handle').returns(Promise.resolve()); @@ -53,10 +54,10 @@ describe('Server', function() { describe('token()', function() { it('should call `handle`', function() { - const model = { + const model = Model.from({ getClient: function() {}, saveToken: function() {} - }; + }); const server = new Server({ model: model }); sinon.stub(TokenHandler.prototype, 'handle').returns(Promise.resolve());