diff --git a/.circleci/authReact.sh b/.circleci/authReact.sh index 207ae3c7d..4b6e06290 100755 --- a/.circleci/authReact.sh +++ b/.circleci/authReact.sh @@ -23,59 +23,94 @@ done <<< "$version" coreDriverVersion=`echo $coreDriverArray | jq ". | last"` coreDriverVersion=`echo $coreDriverVersion | tr -d '"'` -coreFree=`curl -s -X GET \ -"https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $coreFree | jq .core` == "null" ]] +coreFree="null" + +if [ -f cdi-core-map.json ] then - echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." - exit 1 + cat cdi-core-map.json + echo "coreDriverVersion: $coreDriverVersion" + + coreBranchName=`cat cdi-core-map.json | jq -r '.["'$coreDriverVersion'"]'` + if [ "$coreBranchName" != "null" ] + then + coreFree=$coreDriverVersion + fi +fi + +if [ "$coreFree" == "null" ] +then + coreFree=`curl -s -X GET \ + "https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $coreFree | jq .core` == "null" ]] + then + echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." + exit 1 + fi + coreFree=$(echo $coreFree | jq .core | tr -d '"') fi -coreFree=$(echo $coreFree | jq .core | tr -d '"') frontendDriverVersion=$1 frontendDriverVersion=`echo $frontendDriverVersion | tr -d '"'` -nodeVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend-driver-interface/dependency/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendDriverVersion&driverName=node&frontendName=auth-react" \ --H 'api-version: 1'` -if [[ `echo $nodeVersionXY | jq .driver` == "null" ]] +nodeTag="null" +if [ -f fdi-node-map.json ] then - echo "fetching latest X.Y version for driver given frontend-driver-interface X.Y version: $frontendDriverVersion gave response: $nodeVersionXY. Please make sure all relevant drivers have been pushed." - exit 1 + nodeTag=`cat fdi-node-map.json | jq '.["'$frontendDriverVersion'"]' | tr -d '"'` fi -nodeVersionXY=$(echo $nodeVersionXY | jq .driver | tr -d '"') -nodeInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$nodeVersionXY&name=node" \ --H 'api-version: 0'` -if [[ `echo $nodeInfo | jq .tag` == "null" ]] +if [ "$nodeTag" == "null" ] then - echo "fetching latest X.Y.Z version for driver, X.Y version: $nodeVersionXY gave response: $nodeInfo" - exit 1 + nodeVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend-driver-interface/dependency/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendDriverVersion&driverName=node&frontendName=auth-react" \ + -H 'api-version: 1'` + if [[ `echo $nodeVersionXY | jq .driver` == "null" ]] + then + echo "fetching latest X.Y version for driver given frontend-driver-interface X.Y version: $frontendDriverVersion gave response: $nodeVersionXY. Please make sure all relevant drivers have been pushed." + exit 1 + fi + nodeVersionXY=$(echo $nodeVersionXY | jq .driver | tr -d '"') + + nodeInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$nodeVersionXY&name=node" \ + -H 'api-version: 0'` + if [[ `echo $nodeInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for driver, X.Y version: $nodeVersionXY gave response: $nodeInfo" + exit 1 + fi + nodeTag=$(echo $nodeInfo | jq .tag | tr -d '"') fi -nodeTag=$(echo $nodeInfo | jq .tag | tr -d '"') -frontendAuthReactVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend-driver-interface/dependency/frontend/latest?password=$SUPERTOKENS_API_KEY&frontendName=auth-react&mode=DEV&version=$frontendDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $frontendAuthReactVersionXY | jq .frontend` == "null" ]] +frontendAuthReactTag="null" +if [ -f fdi-auth-react-map.json ] then - echo "fetching latest X.Y version for frontend given frontend-driver-interface X.Y version: $frontendDriverVersion, name: auth-react gave response: $frontend. Please make sure all relevant frontend libs have been pushed." - exit 1 + frontendAuthReactTag=`cat fdi-auth-react-map.json | jq '.["'$frontendDriverVersion'"]' | tr -d '"'` fi -frontendAuthReactVersionXY=$(echo $frontendAuthReactVersionXY | jq .frontend | tr -d '"') -frontendAuthReactInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendAuthReactVersionXY&name=auth-react" \ --H 'api-version: 0'` -if [[ `echo $frontendAuthReactInfo | jq .tag` == "null" ]] +if [ "$frontendAuthReactTag" == "null" ] then - echo "fetching latest X.Y.Z version for frontend, X.Y version: $frontendAuthReactVersionXY gave response: $frontendAuthReactInfo" - exit 1 + frontendAuthReactVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend-driver-interface/dependency/frontend/latest?password=$SUPERTOKENS_API_KEY&frontendName=auth-react&mode=DEV&version=$frontendDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $frontendAuthReactVersionXY | jq .frontend` == "null" ]] + then + echo "fetching latest X.Y version for frontend given frontend-driver-interface X.Y version: $frontendDriverVersion, name: auth-react gave response: $frontend. Please make sure all relevant frontend libs have been pushed." + exit 1 + fi + frontendAuthReactVersionXY=$(echo $frontendAuthReactVersionXY | jq .frontend | tr -d '"') + + frontendAuthReactInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendAuthReactVersionXY&name=auth-react" \ + -H 'api-version: 0'` + if [[ `echo $frontendAuthReactInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for frontend, X.Y version: $frontendAuthReactVersionXY gave response: $frontendAuthReactInfo" + exit 1 + fi + frontendAuthReactTag=$(echo $frontendAuthReactInfo | jq .tag | tr -d '"') + frontendAuthReactVersion=$(echo $frontendAuthReactInfo | jq .version | tr -d '"') fi -frontendAuthReactTag=$(echo $frontendAuthReactInfo | jq .tag | tr -d '"') -frontendAuthReactVersion=$(echo $frontendAuthReactInfo | jq .version | tr -d '"') if [[ $frontendDriverVersion == '1.3' || $frontendDriverVersion == '1.8' ]]; then # we skip this since the tests for auth-react here are not reliable due to race conditions... diff --git a/.circleci/config.yml b/.circleci/config.yml index 78230c4c8..916749778 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,27 @@ orbs: continuation: circleci/continuation@0.1.2 slack: circleci/slack@3.4.2 jq: circleci/jq@2.2.0 + +parameters: + force: + type: boolean + default: false + cdi-core-map: + type: string + default: "{}" + cdi-plugin-interface-map: + type: string + default: "{}" + fdi-node-map: + type: string + default: "{}" + fdi-auth-react-map: + type: string + default: "{}" + fdi-website-map: + type: string + default: "{}" + jobs: publish: docker: @@ -36,7 +57,7 @@ jobs: - run: name: Generate config command: | - cd .circleci && ./generateConfig.sh + cd .circleci && ./generateConfig.sh << pipeline.parameters.force >> '<< pipeline.parameters.cdi-core-map >>' '<< pipeline.parameters.cdi-plugin-interface-map >>' '<< pipeline.parameters.fdi-node-map >>' '<< pipeline.parameters.fdi-auth-react-map >>' '<< pipeline.parameters.fdi-website-map >>' - continuation/continue: configuration_path: .circleci/config_continue.yml # use newly generated config to continue @@ -52,12 +73,7 @@ workflows: only: /v[0-9]+(\.[0-9]+)*/ branches: ignore: /.*/ - - setup: - filters: - tags: - only: /dev-v[0-9]+(\.[0-9]+)*/ - branches: - only: /test-cicd\/.*/ + - setup - update-docs: context: - slack-notification diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 7a2e18b78..b250913d9 100644 --- a/.circleci/config_continue.yml +++ b/.circleci/config_continue.yml @@ -4,6 +4,27 @@ orbs: continuation: circleci/continuation@0.1.2 slack: circleci/slack@3.4.2 jq: circleci/jq@2.2.0 + +parameters: + force: + type: boolean + default: false + cdi-core-map: + type: string + default: "{}" + cdi-plugin-interface-map: + type: string + default: "{}" + fdi-node-map: + type: string + default: "{}" + fdi-auth-react-map: + type: string + default: "{}" + fdi-website-map: + type: string + default: "{}" + jobs: test-dev-tag-as-not-passed: docker: @@ -12,16 +33,16 @@ jobs: - run: echo "Testing branch << pipeline.git.branch >>" - when: condition: - not: - matches: - pattern: "^test-cicd/.*$" - value: << pipeline.git.branch >> + matches: + pattern: "^[0-9]+\\.[0-9]+$" # X.Y branches + value: << pipeline.git.branch >> steps: - checkout - run: (cd .circleci/ && ./markDevTagAsTestNotPassed.sh) test-unit: docker: - image: rishabhpoddar/supertokens_node_driver_testing_node_20 + - image: rishabhpoddar/oauth-server-cicd resource_class: large parameters: cdi-version: @@ -39,6 +60,7 @@ jobs: test-backend-sdk-testing: docker: - image: rishabhpoddar/supertokens_node_driver_testing_node_20 + - image: rishabhpoddar/oauth-server-cicd resource_class: large parameters: cdi-version: @@ -57,6 +79,7 @@ jobs: test-website: docker: - image: rishabhpoddar/supertokens_website_sdk_testing + - image: rishabhpoddar/oauth-server-cicd resource_class: large parameters: fdi-version: @@ -75,6 +98,7 @@ jobs: test-authreact: docker: - image: rishabhpoddar/supertokens_website_sdk_testing_node_16 + - image: rishabhpoddar/oauth-server-cicd resource_class: large parameters: fdi-version: @@ -92,6 +116,12 @@ jobs: - store_artifacts: path: test_report/backend.log destination: logs + - store_artifacts: + path: test_report/screenshots + destination: screenshots + - store_artifacts: + path: test_report/react-logs + destination: react-logs - slack/status test-success: docker: @@ -100,10 +130,9 @@ jobs: - run: echo "Testing passed for branch << pipeline.git.branch >>" - when: condition: - not: - matches: - pattern: "^test-cicd/.*$" - value: << pipeline.git.branch >> + matches: + pattern: "^[0-9]+\\.[0-9]+$" # X.Y branches + value: << pipeline.git.branch >> steps: - checkout - run: (cd .circleci/ && ./markAsSuccess.sh) diff --git a/.circleci/configs/hydra.yml b/.circleci/configs/hydra.yml new file mode 100644 index 000000000..088ed79f4 --- /dev/null +++ b/.circleci/configs/hydra.yml @@ -0,0 +1,22 @@ +serve: + cookies: + same_site_mode: Lax + +urls: + self: + issuer: http://localhost:4444 + consent: http://localhost:3001/auth/oauth/consent + login: http://localhost:3001/auth/oauth/login + logout: http://localhost:3001/auth/oauth/logout + +secrets: + system: + - youReallyNeedToChangeThis + +oidc: + subject_identifiers: + supported_types: + - pairwise + - public + pairwise: + salt: youReallyNeedToChangeThis diff --git a/.circleci/doBackendSDKTests.sh b/.circleci/doBackendSDKTests.sh index 984a0b79b..a145a4433 100755 --- a/.circleci/doBackendSDKTests.sh +++ b/.circleci/doBackendSDKTests.sh @@ -8,17 +8,33 @@ fi coreDriverVersion=$1 coreDriverVersion=`echo $coreDriverVersion | tr -d '"'` -frontendDriverVersion=$2 +frontendDriverVersion=`echo $2 | tr -d '"'` -coreFree=`curl -s -X GET \ -"https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $coreFree | jq .core` == "null" ]] +coreFree="null" +if [ -f cdi-core-map.json ] then - echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." - exit 1 + cat cdi-core-map.json + echo "coreDriverVersion: $coreDriverVersion" + + coreBranchName=`cat cdi-core-map.json | jq -r '.["'$coreDriverVersion'"]'` + if [ "$coreBranchName" != "null" ] + then + coreFree=$coreDriverVersion + fi +fi + +if [ "$coreFree" == "null" ] +then + coreFree=`curl -s -X GET \ + "https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $coreFree | jq .core` == "null" ]] + then + echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." + exit 1 + fi + coreFree=$(echo $coreFree | jq .core | tr -d '"') fi -coreFree=$(echo $coreFree | jq .core | tr -d '"') cd .. ./test/testExports.sh diff --git a/.circleci/doUnitTests.sh b/.circleci/doUnitTests.sh index a59e4a916..000f0d79b 100755 --- a/.circleci/doUnitTests.sh +++ b/.circleci/doUnitTests.sh @@ -8,15 +8,31 @@ fi coreDriverVersion=$1 coreDriverVersion=`echo $coreDriverVersion | tr -d '"'` -coreFree=`curl -s -X GET \ -"https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $coreFree | jq .core` == "null" ]] +coreFree="null" +if [ -f cdi-core-map.json ] then - echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." - exit 1 + cat cdi-core-map.json + echo "coreDriverVersion: $coreDriverVersion" + + coreBranchName=`cat cdi-core-map.json | jq -r '.["'$coreDriverVersion'"]'` + if [ "$coreBranchName" != "null" ] + then + coreFree=$coreDriverVersion + fi +fi + +if [ "$coreFree" == "null" ] +then + coreFree=`curl -s -X GET \ + "https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $coreFree | jq .core` == "null" ]] + then + echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." + exit 1 + fi + coreFree=$(echo $coreFree | jq .core | tr -d '"') fi -coreFree=$(echo $coreFree | jq .core | tr -d '"') cd .. ./test/testExports.sh diff --git a/.circleci/forceRunCI.sh b/.circleci/forceRunCI.sh new file mode 100755 index 000000000..8ba66a2a6 --- /dev/null +++ b/.circleci/forceRunCI.sh @@ -0,0 +1,23 @@ +PAT=`cat .pat` +auth=`echo "${PAT}:" | tr -d '\n' | base64 --wrap=0` +branch=`git rev-parse --abbrev-ref HEAD` + +cdiCoreMap='{ }' +cdiPluginInterfaceMap='{ }' +fdiNodeMap='{ }' +fdiWebsiteMap='{ }' +fdiAuthReactMap='{ }' + +data=`jq -cn --arg branch "$branch" \ + --arg cdiCoreMap "$cdiCoreMap" \ + --arg cdiPluginInterfaceMap "$cdiPluginInterfaceMap" \ + --arg fdiNodeMap "$fdiNodeMap" \ + --arg fdiWebsiteMap "$fdiWebsiteMap" \ + --arg fdiAuthReactMap "$fdiAuthReactMap" \ + '{ branch: $ARGS.named.branch, parameters: { force: true, "cdi-core-map": $ARGS.named.cdiCoreMap, "cdi-plugin-interface-map": $ARGS.named.cdiPluginInterfaceMap, "fdi-node-map": $ARGS.named.fdiNodeMap, "fdi-website-map": $ARGS.named.fdiWebsiteMap, "fdi-auth-react-map": $ARGS.named.fdiAuthReactMap }}'` + +curl --request POST \ + --url 'https://circleci.com/api/v2/project/gh/supertokens/supertokens-node/pipeline' \ + --header "authorization: Basic $auth" \ + --header 'content-type: application/json' \ + --data "$data" diff --git a/.circleci/generateConfig.sh b/.circleci/generateConfig.sh index 1dfffd6b0..1f08d9099 100755 --- a/.circleci/generateConfig.sh +++ b/.circleci/generateConfig.sh @@ -11,3 +11,12 @@ fi sed -i -e 's/cdi-version: placeholder/cdi-version: '`printf "%q" $coreDriverArray`'/' config_continue.yml sed -i -e 's/fdi-version: placeholder/fdi-version: '`printf "%q" $frontendDriverArray`'/' config_continue.yml + +if [ "$1" = "true" ]; then + sed -i -e 's/test-cicd\\\/\.\*/.*/' config_continue.yml + sed -i -e "s@^ - checkout@ - checkout\n - run: 'echo ''$2'' >> .circleci\/cdi-core-map.json'@" config_continue.yml + sed -i -e "s@^ - checkout@ - checkout\n - run: 'echo ''$3'' >> .circleci\/cdi-plugin-interface-map.json'@" config_continue.yml + sed -i -e "s@^ - checkout@ - checkout\n - run: 'echo ''$4'' >> .circleci\/fdi-node-map.json'@" config_continue.yml + sed -i -e "s@^ - checkout@ - checkout\n - run: 'echo ''$5'' >> .circleci\/fdi-auth-react-map.json'@" config_continue.yml + sed -i -e "s@^ - checkout@ - checkout\n - run: 'echo ''$6'' >> .circleci\/fdi-website-map.json'@" config_continue.yml +fi diff --git a/.circleci/setupAndTestBackendSDKWithFreeCore.sh b/.circleci/setupAndTestBackendSDKWithFreeCore.sh index a4a563b0b..b139658fb 100755 --- a/.circleci/setupAndTestBackendSDKWithFreeCore.sh +++ b/.circleci/setupAndTestBackendSDKWithFreeCore.sh @@ -1,34 +1,63 @@ -coreInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $coreInfo | jq .tag` == "null" ]] +coreVersionXYParam=`echo $1 | tr -d '"'` +coreVersionXY=$coreVersionXYParam + +if [ -f "cdi-core-map.json" ] then - echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" - exit 1 + coreTag=`cat cdi-core-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$coreTag" != "null" ] + then + coreVersion=$coreTag + coreVersionXY=$coreTag + fi fi -coreTag=$(echo $coreInfo | jq .tag | tr -d '"') -coreVersion=$(echo $coreInfo | jq .version | tr -d '"') -pluginInterfaceVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] +if [ -z "$coreVersion" ] then - echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" - exit 1 + coreInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $coreInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" + exit 1 + fi + coreTag=$(echo $coreInfo | jq .tag | tr -d '"') + coreVersion=$(echo $coreInfo | jq .version | tr -d '"') fi -pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') -pluginInterfaceInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] +if [ -f "cdi-plugin-interface-map.json" ] then - echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" - exit 1 + pluginInterfaceTag=`cat cdi-plugin-interface-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$pluginInterfaceTag" != "null" ] + then + pluginInterfaceVersionXY=$pluginInterfaceTag + pluginInterfaceVersion=$pluginInterfaceTag + fi +fi + +if [ -z "$pluginInterfaceVersion" ] +then + pluginInterfaceVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] + then + echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" + exit 1 + fi + pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') + + pluginInterfaceInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" + exit 1 + fi + pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') + pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') fi -pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') -pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') echo "Testing with FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" @@ -39,10 +68,16 @@ if [[ $2 == "2.0" ]] || [[ $2 == "2.1" ]] || [[ $2 == "2.2" ]] then git checkout 36e5af1b9a4e3b07247d0cf333cf82a071a78681 fi -echo -e "core,$1\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt +echo -e "core,$coreVersionXY\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt ./loadModules --ssh cd supertokens-core git checkout $coreTag + +# Update oauth provider config in devConfig.yaml +sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml +sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml +sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml + cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag cd ../ diff --git a/.circleci/setupAndTestWithAuthReact.sh b/.circleci/setupAndTestWithAuthReact.sh index b7f3de11e..61ae3f2af 100755 --- a/.circleci/setupAndTestWithAuthReact.sh +++ b/.circleci/setupAndTestWithAuthReact.sh @@ -1,44 +1,79 @@ -coreInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $coreInfo | jq .tag` == "null" ]] +coreVersionXYParam=`echo $1 | tr -d '"'` +coreVersionXY=$coreVersionXYParam + +if [ -f "cdi-core-map.json" ] then - echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" - exit 1 + coreTag=`cat cdi-core-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$coreTag" != "null" ] + then + coreVersion=$coreTag + coreVersionXY=$coreTag + fi fi -coreTag=$(echo $coreInfo | jq .tag | tr -d '"') -coreVersion=$(echo $coreInfo | jq .version | tr -d '"') -pluginInterfaceVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] +if [ -z "$coreVersion" ] then - echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" - exit 1 + coreInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $coreInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" + exit 1 + fi + coreTag=$(echo $coreInfo | jq .tag | tr -d '"') + coreVersion=$(echo $coreInfo | jq .version | tr -d '"') fi -pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') -pluginInterfaceInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] +if [ -f "cdi-plugin-interface-map.json" ] then - echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" - exit 1 + pluginInterfaceTag=`cat cdi-plugin-interface-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$pluginInterfaceTag" != "null" ] + then + pluginInterfaceVersionXY=$pluginInterfaceTag + pluginInterfaceVersion=$pluginInterfaceTag + fi +fi + +if [ -z "$pluginInterfaceVersion" ] +then + pluginInterfaceVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] + then + echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" + exit 1 + fi + pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') + + pluginInterfaceInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" + exit 1 + fi + pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') + pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') fi -pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') -pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') echo "Testing with frontend auth-react: $2, node tag: $3, FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" cd ../../ git clone git@github.com:supertokens/supertokens-root.git cd supertokens-root -echo -e "core,$1\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt +echo -e "core,$coreVersionXY\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt ./loadModules --ssh cd supertokens-core git checkout $coreTag + +# Update oauth provider config in devConfig.yaml +sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml +sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml +sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml + cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag cd ../ @@ -64,7 +99,7 @@ npm i mkdir -p ../../test_report echo "Testing with frontend auth-react: $2, node tag: $3, FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" >> ../../test_report/backend.log -DEBUG=com.supertokens TEST_MODE=testing node . >> ../../test_report/backend.log 2>&1 & +NODE_PORT=8083 DEBUG=com.supertokens TEST_MODE=testing node . >> ../../test_report/backend.log 2>&1 & pid=$! cd ../../../supertokens-auth-react/ @@ -75,9 +110,15 @@ cd ../../../supertokens-auth-react/ # flag will not be checked because Auth0 is used as a provider so that the Thirdparty tests can run reliably. # In versions lower than 0.18 Github is used as the provider. -MOCHA_FILE=test_report/report_node.xml SKIP_OAUTH=true npm run test-with-non-node +DEBUG=com.supertokens MOCHA_FILE=test_report/report_node.xml SKIP_OAUTH=true npm run test-with-non-node if [[ $? -ne 0 ]] then + mkdir -p ../project/test_report/screenshots + mv ./test_report/screenshots/* ../project/test_report/screenshots/ + + mkdir -p ../project/test_report/react-logs + mv ./test_report/logs/* ../project/test_report/react-logs/ + echo "test failed... exiting!" rm -rf ./test/server/node_modules/supertokens-node git checkout HEAD -- ./test/server/package.json diff --git a/.circleci/setupAndTestWithFreeCore.sh b/.circleci/setupAndTestWithFreeCore.sh index 83b0f8201..bda9aa416 100755 --- a/.circleci/setupAndTestWithFreeCore.sh +++ b/.circleci/setupAndTestWithFreeCore.sh @@ -1,35 +1,63 @@ -coreInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $coreInfo | jq .tag` == "null" ]] +coreVersionXYParam=`echo $1 | tr -d '"'` +coreVersionXY=$coreVersionXYParam + +if [ -f "cdi-core-map.json" ] then - echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" - exit 1 + coreTag=`cat cdi-core-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$coreTag" != "null" ] + then + coreVersion=$coreTag + coreVersionXY=$coreTag + fi fi -coreTag=$(echo $coreInfo | jq .tag | tr -d '"') -coreVersion=$(echo $coreInfo | jq .version | tr -d '"') -pluginInterfaceVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] +if [ -z "$coreVersion" ] then - echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" - exit 1 + coreInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $coreInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" + exit 1 + fi + coreTag=$(echo $coreInfo | jq .tag | tr -d '"') + coreVersion=$(echo $coreInfo | jq .version | tr -d '"') fi -pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') -pluginInterfaceInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] +if [ -f "cdi-plugin-interface-map.json" ] then - echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" - exit 1 + pluginInterfaceTag=`cat cdi-plugin-interface-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$pluginInterfaceTag" != "null" ] + then + pluginInterfaceVersionXY=$pluginInterfaceTag + pluginInterfaceVersion=$pluginInterfaceTag + fi fi -pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') -pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') +if [ -z "$pluginInterfaceVersion" ] +then + pluginInterfaceVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] + then + echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" + exit 1 + fi + pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') + + pluginInterfaceInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" + exit 1 + fi + pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') + pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') +fi echo "Testing with FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" cd ../../ @@ -39,10 +67,16 @@ if [[ $2 == "2.0" ]] || [[ $2 == "2.1" ]] || [[ $2 == "2.2" ]] then git checkout 36e5af1b9a4e3b07247d0cf333cf82a071a78681 fi -echo -e "core,$1\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt +echo -e "core,$coreVersionXY\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt ./loadModules --ssh cd supertokens-core git checkout $coreTag + +# Update oauth provider config in devConfig.yaml +sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml +sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml +sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml + cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag cd ../ diff --git a/.circleci/setupAndTestWithFrontend.sh b/.circleci/setupAndTestWithFrontend.sh index 8e870d4bb..9721c7a6e 100755 --- a/.circleci/setupAndTestWithFrontend.sh +++ b/.circleci/setupAndTestWithFrontend.sh @@ -1,44 +1,79 @@ -coreInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $coreInfo | jq .tag` == "null" ]] +coreVersionXYParam=`echo $1 | tr -d '"'` +coreVersionXY=$coreVersionXYParam + +if [ -f "cdi-core-map.json" ] then - echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" - exit 1 + coreTag=`cat cdi-core-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$coreTag" != "null" ] + then + coreVersion=$coreTag + coreVersionXY=$coreTag + fi fi -coreTag=$(echo $coreInfo | jq .tag | tr -d '"') -coreVersion=$(echo $coreInfo | jq .version | tr -d '"') -pluginInterfaceVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] +if [ -z "$coreVersion" ] then - echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" - exit 1 + coreInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $coreInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" + exit 1 + fi + coreTag=$(echo $coreInfo | jq .tag | tr -d '"') + coreVersion=$(echo $coreInfo | jq .version | tr -d '"') fi -pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') -pluginInterfaceInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ --H 'api-version: 0'` -if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] +if [ -f "cdi-plugin-interface-map.json" ] then - echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" - exit 1 + pluginInterfaceTag=`cat cdi-plugin-interface-map.json | jq '.["'$coreVersionXYParam'"]' | tr -d '"'` + if [ "$pluginInterfaceTag" != "null" ] + then + pluginInterfaceVersionXY=$pluginInterfaceTag + pluginInterfaceVersion=$pluginInterfaceTag + fi +fi + +if [ -z "$pluginInterfaceVersion" ] +then + pluginInterfaceVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] + then + echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" + exit 1 + fi + pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') + + pluginInterfaceInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ + -H 'api-version: 0'` + if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" + exit 1 + fi + pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') + pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') fi -pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') -pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') echo "Testing with frontend website: $2, FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" cd ../../ git clone git@github.com:supertokens/supertokens-root.git cd supertokens-root -echo -e "core,$1\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt +echo -e "core,$coreVersionXY\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt ./loadModules --ssh cd supertokens-core git checkout $coreTag + +# Update oauth provider config in devConfig.yaml +sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml +sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml +sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml + cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag cd ../ diff --git a/.circleci/website.sh b/.circleci/website.sh index 0480b2214..587dc1bae 100755 --- a/.circleci/website.sh +++ b/.circleci/website.sh @@ -23,59 +23,95 @@ done <<< "$version" coreDriverVersion=`echo $coreDriverArray | jq ". | last"` coreDriverVersion=`echo $coreDriverVersion | tr -d '"'` -coreFree=`curl -s -X GET \ -"https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $coreFree | jq .core` == "null" ]] +coreFree="null" + +if [ -f cdi-core-map.json ] +then + cat cdi-core-map.json + echo "coreDriverVersion: $coreDriverVersion" + + coreBranchName=`cat cdi-core-map.json | jq -r '.["'$coreDriverVersion'"]'` + if [ "$coreBranchName" != "null" ] + then + coreFree=$coreDriverVersion + fi +fi + +if [ "$coreFree" == "null" ] then - echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." - exit 1 + coreFree=`curl -s -X GET \ + "https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $coreFree | jq .core` == "null" ]] + then + echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." + exit 1 + fi + coreFree=$(echo $coreFree | jq .core | tr -d '"') fi -coreFree=$(echo $coreFree | jq .core | tr -d '"') + frontendDriverVersion=$1 frontendDriverVersion=`echo $frontendDriverVersion | tr -d '"'` -frontendVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend-driver-interface/dependency/frontend/latest?password=$SUPERTOKENS_API_KEY&frontendName=website&mode=DEV&version=$frontendDriverVersion&driverName=node" \ --H 'api-version: 1'` -if [[ `echo $frontendVersionXY | jq .frontend` == "null" ]] +frontendTag="null" +if [ -f fdi-website-map.json ] then - echo "fetching latest X.Y version for frontend given frontend-driver-interface X.Y version: $frontendDriverVersion, name: website gave response: $frontend. Please make sure all relevant versions have been pushed." - exit 1 + frontendTag=`cat fdi-website-map.json | jq '.["'$frontendDriverVersion'"]' | tr -d '"'` fi -frontendVersionXY=$(echo $frontendVersionXY | jq .frontend | tr -d '"') -frontendInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendVersionXY&name=website" \ --H 'api-version: 0'` -if [[ `echo $frontendInfo | jq .tag` == "null" ]] +if [ "$frontendTag" == "null" ] then - echo "fetching latest X.Y.Z version for frontend, X.Y version: $frontendVersionXY gave response: $frontendInfo" - exit 1 + frontendVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend-driver-interface/dependency/frontend/latest?password=$SUPERTOKENS_API_KEY&frontendName=website&mode=DEV&version=$frontendDriverVersion&driverName=node" \ + -H 'api-version: 1'` + if [[ `echo $frontendVersionXY | jq .frontend` == "null" ]] + then + echo "fetching latest X.Y version for frontend given frontend-driver-interface X.Y version: $frontendDriverVersion, name: website gave response: $frontend. Please make sure all relevant versions have been pushed." + exit 1 + fi + frontendVersionXY=$(echo $frontendVersionXY | jq .frontend | tr -d '"') + + frontendInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendVersionXY&name=website" \ + -H 'api-version: 0'` + if [[ `echo $frontendInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for frontend, X.Y version: $frontendVersionXY gave response: $frontendInfo" + exit 1 + fi + frontendTag=$(echo $frontendInfo | jq .tag | tr -d '"') + frontendVersion=$(echo $frontendInfo | jq .version | tr -d '"') fi -frontendTag=$(echo $frontendInfo | jq .tag | tr -d '"') -frontendVersion=$(echo $frontendInfo | jq .version | tr -d '"') -nodeVersionXY=`curl -s -X GET \ -"https://api.supertokens.io/0/frontend-driver-interface/dependency/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendDriverVersion&driverName=node&frontendName=website" \ --H 'api-version: 1'` -if [[ `echo $nodeVersionXY | jq .driver` == "null" ]] +nodeTag="null" +if [ -f fdi-node-map.json ] then - echo "fetching latest X.Y version for driver given frontend-driver-interface X.Y version: $frontendDriverVersion gave response: $nodeVersionXY. Please make sure all relevant drivers have been pushed." - exit 1 + nodeTag=`cat fdi-node-map.json | jq '.["'$frontendDriverVersion'"]' | tr -d '"'` fi -nodeVersionXY=$(echo $nodeVersionXY | jq .driver | tr -d '"') -nodeInfo=`curl -s -X GET \ -"https://api.supertokens.io/0/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$nodeVersionXY&name=node" \ --H 'api-version: 0'` -if [[ `echo $nodeInfo | jq .tag` == "null" ]] +if [ "$nodeTag" == "null" ] then - echo "fetching latest X.Y.Z version for driver, X.Y version: $nodeVersionXY gave response: $nodeInfo" - exit 1 + nodeVersionXY=`curl -s -X GET \ + "https://api.supertokens.io/0/frontend-driver-interface/dependency/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$frontendDriverVersion&driverName=node&frontendName=website" \ + -H 'api-version: 1'` + if [[ `echo $nodeVersionXY | jq .driver` == "null" ]] + then + echo "fetching latest X.Y version for driver given frontend-driver-interface X.Y version: $frontendDriverVersion gave response: $nodeVersionXY. Please make sure all relevant drivers have been pushed." + exit 1 + fi + nodeVersionXY=$(echo $nodeVersionXY | jq .driver | tr -d '"') + + nodeInfo=`curl -s -X GET \ + "https://api.supertokens.io/0/driver/latest?password=$SUPERTOKENS_API_KEY&mode=DEV&version=$nodeVersionXY&name=node" \ + -H 'api-version: 0'` + if [[ `echo $nodeInfo | jq .tag` == "null" ]] + then + echo "fetching latest X.Y.Z version for driver, X.Y version: $nodeVersionXY gave response: $nodeInfo" + exit 1 + fi + nodeTag=$(echo $nodeInfo | jq .tag | tr -d '"') fi -nodeTag=$(echo $nodeInfo | jq .tag | tr -d '"') tries=1 while [ $tries -le 3 ] diff --git a/.gitignore b/.gitignore index a5e771d2c..cfbfc38b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules +test/test-server/node_modules /examples/**/node_modules .DS_Store /.history @@ -12,4 +13,5 @@ releasePassword /test_report /temp_test_exports /temp_* -/.nyc_output \ No newline at end of file +/.nyc_output +.circleci/.pat diff --git a/CHANGELOG.md b/CHANGELOG.md index a29041432..c7b92c113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [21.0.0] - 2024-10-07 + +- Added OAuth2Provider recipe +- Added a way to run CI on unmerged PRs +- Added support for FDIs: 3.1 and 4.0. Required by: auth-react >=0.49.0 and web-js>=0.15.0 +- The `networkInterceptor` now also gets a new `params` prop in the request config. + +### Breaking change + +- Changes type of value in formField object to be `unknown` instead of `string` to add support for accepting any type of value in form fields. +- Only supporting CDI 5.2, Compatible with Core version >= 10.0 +- Changed the default value of `overwriteSessionDuringSignInUp` to true. +- Added a new `shouldTryLinkingWithSessionUser` to sign in/up related APIs (and the related recipe functions) + - This will default to false on the API + - This will be set to true in function calls if you pass a session, otherwise it is set to false + - By setting this to true you can enable MFA flows (trying to connect to the session user) + - If set to false, the sign-in/up will be considered a first-factor + - Changed APIs: + - `ThirdParty.signInUpPOST` + - `Passwordless.createCodePOST` + - `Passwordless.consumeCodePOST` + - `Passwordless.consumeCodePOST` + - Changed functions: + - `ThirdParty.signInUp` + - `ThirdPary.manuallyCreateOrUpdateUser` + - `Passwordless.createCode` + - `Passwordless.consumeCode` +- We no longer try to load the session if `shouldTryLinkingWithSessionUser` is set to false and overwriteSessionDuringSignInUp is set to true or left as the default value. +- Changed the return type of `getOpenIdConfiguration` and `getOpenIdDiscoveryConfigurationGET`, and added the following props: + - authorization_endpoint + - token_endpoint + - userinfo_endpoint + - revocation_endpoint + - token_introspection_endpoint + - end_session_endpoint + - subject_types_supported + - id_token_signing_alg_values_supported + - response_types_supported +- Exposing the OpenId recipe separately and remove it from the Session recipe + - This means that we removed `override.openIdFeature` from the Session recipe configuration +- Removed `getJWKS` from the OpenId recipe, as it is already exposed by the JWT recipe +- We now automatically initialize the OpenId and JWT recipes even if you do not use the Session recipe + +### Migration + +#### Separating the OpenId recipe from Session recipe + +If you used to use the `openIdFeature` in the Session recipe, you should now use the OpenId recipe directly instead: + +Before: + +```tsx +import SuperTokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + override: { + openIdFeature: { + jwtFeature: { + functions: originalImplementation => ({ + ...originalImplementation, + getJWKS: async (input) => { + console.log("getJWKS called"); + return originalImplementation.getJWKS(input); + }, + }) + }, + functions: originalImplementation => ({ + ...originalImplementation, + getOpenIdDiscoveryConfiguration: async (input) => ({ + issuer: "your issuer", + jwks_uri: "https://your.api.domain/auth/jwt/jwks.json", + status: "OK" + }), + }) + } + } + }); + ], +}); +``` + +After: + +```tsx +import SuperTokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import OpenId from "supertokens-node/recipe/openid"; +import JWT from "supertokens-node/recipe/jwt"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + JWT.init({ + override: { + functions: originalImplementation => ({ + ...originalImplementation, + getJWKS: async (input) => { + console.log("getJWKS called"); + return originalImplementation.getJWKS(input); + }, + }) + } + }), + OpenId.init({ + override: { + functions: originalImplementation => ({ + ...originalImplementation, + getOpenIdDiscoveryConfiguration: async (input) => ({ + issuer: "your issuer", + jwks_uri: "https://your.api.domain/auth/jwt/jwks.json", + status: "OK" + }), + }) + } + }); + ], +}); +``` + ## [20.1.3] - 2024-09-30 - Replaces `psl` with `tldts` to avoid `punycode` deprecation warning. @@ -344,6 +476,10 @@ for (const tenant of tenantsRes.tenants) { - `refreshPOST` and `refreshSession` now clears all user tokens upon CSRF failures and if no tokens are found. See the latest comment on https://github.com/supertokens/supertokens-node/issues/141 for more details. +## [18.0.2] - 2024-07-09 + +- `refreshPOST` and `refreshSession` now clears all user tokens upon CSRF failures and if no tokens are found. See the latest comment on https://github.com/supertokens/supertokens-node/issues/141 for more details. + ## [18.0.1] - 2024-06-19 ### Fixes diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index f4bea3ba7..0f9bab285 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of core-driver interfaces branch names that this core supports", - "versions": ["5.1"] + "versions": ["5.2"] } diff --git a/frontendDriverInterfaceSupported.json b/frontendDriverInterfaceSupported.json index c925842b1..b5454981e 100644 --- a/frontendDriverInterfaceSupported.json +++ b/frontendDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of frontend-driver interfaces branch names that this core supports", - "versions": ["1.17", "1.18", "1.19", "2.0", "3.0"] + "versions": ["1.17", "1.18", "1.19", "2.0", "3.0", "3.1", "4.0"] } diff --git a/lib/build/authUtils.d.ts b/lib/build/authUtils.d.ts index 9e714eb91..98ff4ad41 100644 --- a/lib/build/authUtils.d.ts +++ b/lib/build/authUtils.d.ts @@ -54,6 +54,7 @@ export declare const AuthUtils: { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }: { authenticatingAccountInfo: AccountInfoWithRecipeId; @@ -65,6 +66,7 @@ export declare const AuthUtils: { signInVerifiesLoginMethod: boolean; skipSessionUserUpdateInCore: boolean; session?: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }) => Promise< | { @@ -194,6 +196,7 @@ export declare const AuthUtils: { */ checkAuthTypeAndLinkingStatus: ( session: SessionContainerInterface | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined, accountInfo: AccountInfoWithRecipeId, inputUser: User | undefined, skipSessionUserUpdateInCore: boolean, @@ -235,17 +238,19 @@ export declare const AuthUtils: { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }: { tenantId: string; inputUser: User; recipeUserId: RecipeUserId; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }) => Promise< | { @@ -335,4 +340,10 @@ export declare const AuthUtils: { hasSession: boolean, userContext: UserContext ) => Promise; + loadSessionInAuthAPIIfNeeded: ( + req: BaseRequest, + res: BaseResponse, + shouldTryLinkingWithSessionUser: boolean | undefined, + userContext: UserContext + ) => Promise; }; diff --git a/lib/build/authUtils.js b/lib/build/authUtils.js index a0afc304a..b03e8cb94 100644 --- a/lib/build/authUtils.js +++ b/lib/build/authUtils.js @@ -78,6 +78,7 @@ exports.AuthUtils = { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }) { let validFactorIds; @@ -91,6 +92,7 @@ exports.AuthUtils = { // We also load the session user here if it is available. const authTypeInfo = await exports.AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authenticatingAccountInfo, authenticatingUser, skipSessionUserUpdateInCore, @@ -233,8 +235,9 @@ exports.AuthUtils = { // If the new user wasn't linked to the current one, we check the config and overwrite the session if required // Note: we could also get here if MFA is enabled, but the app didn't want to link the user to the session user. // This is intentional, since the MFA and overwriteSessionDuringSignInUp configs should work independently. - let overwriteSessionDuringSignInUp = recipe_3.default.getInstanceOrThrowError().config - .overwriteSessionDuringSignInUp; + let overwriteSessionDuringSignInUp = recipe_3.default + .getInstanceOrThrowError() + .getNormalisedOverwriteSessionDuringSignInUp(req); if (overwriteSessionDuringSignInUp) { respSession = await session_1.default.createNewSession( req, @@ -255,6 +258,9 @@ exports.AuthUtils = { } } } else { + // We do not have to care about overwriting the session here, since we either: + // - have overwriteSessionDuringSignInUp true and we didn't even try to load the session because we ignore it anyway + // - have overwriteSessionDuringSignInUp false and we checked in the api imlp that there is no session logger_1.logDebugMessage(`postAuthChecks creating session for first factor sign in/up`); // If there is no input session, we do not need to do anything other checks and create a new session respSession = await session_1.default.createNewSession( @@ -439,6 +445,7 @@ exports.AuthUtils = { */ checkAuthTypeAndLinkingStatus: async function ( session, + shouldTryLinkingWithSessionUser, accountInfo, inputUser, skipSessionUserUpdateInCore, @@ -447,21 +454,50 @@ exports.AuthUtils = { logger_1.logDebugMessage(`checkAuthTypeAndLinkingStatus called`); let sessionUser = undefined; if (session === undefined) { + if (shouldTryLinkingWithSessionUser === true) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session not found but shouldTryLinkingWithSessionUser is true", + }); + } logger_1.logDebugMessage( `checkAuthTypeAndLinkingStatus returning first factor because there is no session` ); // If there is no active session we have nothing to link to - so this has to be a first factor sign in return { status: "OK", isFirstFactor: true }; } else { + if (shouldTryLinkingWithSessionUser === false) { + logger_1.logDebugMessage( + `checkAuthTypeAndLinkingStatus returning first factor because shouldTryLinkingWithSessionUser is false` + ); + // In our normal flows this should never happen - but some user overrides might do this. + // Anyway, since shouldTryLinkingWithSessionUser explicitly set to false, it's safe to consider this a firstFactor + return { status: "OK", isFirstFactor: true }; + } if (!utils_3.recipeInitDefinedShouldDoAutomaticAccountLinking(recipe_1.default.getInstance().config)) { - if (recipe_2.default.getInstance() !== undefined) { + if (shouldTryLinkingWithSessionUser === true) { throw new Error( "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" ); } else { - return { status: "OK", isFirstFactor: true }; + // This is the legacy case where shouldTryLinkingWithSessionUser is undefined + if (recipe_2.default.getInstance() !== undefined) { + throw new Error( + "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" + ); + } else { + logger_1.logDebugMessage( + `checkAuthTypeAndLinkingStatus (legacy behaviour) returning first factor because MFA is not initialised and shouldDoAutomaticAccountLinking is not defined` + ); + return { status: "OK", isFirstFactor: true }; + } } } + // If we get here: + // - session is defined + // - shouldTryLinkingWithSessionUser is true or undefined + // - shouldDoAutomaticAccountLinking is defined + // - MFA may or may not be initialized // If the input and the session user are the same if (inputUser !== undefined && inputUser.id === session.getUserId()) { logger_1.logDebugMessage( @@ -489,6 +525,14 @@ exports.AuthUtils = { userContext ); if (sessionUserResult.status === "SHOULD_AUTOMATICALLY_LINK_FALSE") { + if (shouldTryLinkingWithSessionUser === true) { + // tryAndMakeSessionUserIntoAPrimaryUser throws if it is an email verification iss + throw new _1.Error({ + message: + "shouldDoAutomaticAccountLinking returned false when making the session user primary but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true, @@ -519,6 +563,13 @@ exports.AuthUtils = { )}` ); if (shouldLink.shouldAutomaticallyLink === false) { + if (shouldTryLinkingWithSessionUser === true) { + throw new _1.Error({ + message: + "shouldDoAutomaticAccountLinking returned false when making the session user primary but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true }; } else { return { @@ -545,20 +596,22 @@ exports.AuthUtils = { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }) { - logger_1.logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo called"); + logger_1.logDebugMessage("linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo called"); const retry = () => { - logger_1.logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); - return exports.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + logger_1.logDebugMessage("linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); + return exports.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: inputUser, session, + shouldTryLinkingWithSessionUser, recipeUserId, userContext, }); @@ -575,6 +628,7 @@ exports.AuthUtils = { } const authTypeRes = await exports.AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authLoginMethod, inputUser, false, @@ -586,12 +640,12 @@ exports.AuthUtils = { if (authTypeRes.isFirstFactor) { if (!utils_3.recipeInitDefinedShouldDoAutomaticAccountLinking(recipe_1.default.getInstance().config)) { logger_1.logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo skipping link by account info because this is a first factor auth and the app hasn't defined shouldDoAutomaticAccountLinking" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo skipping link by account info because this is a first factor auth and the app hasn't defined shouldDoAutomaticAccountLinking" ); return { status: "OK", user: inputUser }; } logger_1.logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by account info because this is a first factor auth" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by account info because this is a first factor auth" ); // We try and list all users that can be linked to the input user based on the account info // later we can use these when trying to link or when checking if linking to the session user is possible. @@ -616,7 +670,7 @@ exports.AuthUtils = { }; } logger_1.logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by session info" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by session info" ); const sessionLinkingRes = await exports.AuthUtils.tryLinkingBySession({ sessionUser: authTypeRes.sessionUser, @@ -846,6 +900,45 @@ exports.AuthUtils = { } return validFactorIds; }, + loadSessionInAuthAPIIfNeeded: async function (req, res, shouldTryLinkingWithSessionUser, userContext) { + const overwriteSessionDuringSignInUp = recipe_3.default + .getInstanceOrThrowError() + .getNormalisedOverwriteSessionDuringSignInUp(req); + if (shouldTryLinkingWithSessionUser !== false) { + logger_1.logDebugMessage( + "loadSessionInAuthAPIIfNeeded: loading session because shouldTryLinkingWithSessionUser is not set to false so we may want to link later" + ); + return await session_1.default.getSession( + req, + res, + { + // This is optional only if shouldTryLinkingWithSessionUser is undefined + // in the (old) 3.0 FDI, this flag didn't exist and we linking was based on the session presence and shouldDoAutomaticAccountLinking + sessionRequired: shouldTryLinkingWithSessionUser === true, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + } + if (overwriteSessionDuringSignInUp === false) { + logger_1.logDebugMessage( + "loadSessionInAuthAPIIfNeeded: loading session in optional mode because overwriteSessionDuringSignInUp is false so if it is not found we will skip session creation" + ); + return await session_1.default.getSession( + req, + res, + { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + } + logger_1.logDebugMessage( + "loadSessionInAuthAPIIfNeeded: skipping session loading because we are not linking and we would overwrite it anyway" + ); + return undefined; + }, }; async function filterOutInvalidSecondFactorsOrThrowIfAllAreInvalid( factorIds, diff --git a/lib/build/combinedRemoteJWKSet.d.ts b/lib/build/combinedRemoteJWKSet.d.ts new file mode 100644 index 000000000..d36ad16ae --- /dev/null +++ b/lib/build/combinedRemoteJWKSet.d.ts @@ -0,0 +1,21 @@ +// @ts-nocheck +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +export declare function resetCombinedJWKS(): void; +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +export declare function getCombinedJWKS(config: { + jwksRefreshIntervalSec: number; +}): ( + protectedHeader?: import("jose").JWSHeaderParameters | undefined, + token?: import("jose").FlattenedJWSInput | undefined +) => Promise; diff --git a/lib/build/combinedRemoteJWKSet.js b/lib/build/combinedRemoteJWKSet.js new file mode 100644 index 000000000..86d75b126 --- /dev/null +++ b/lib/build/combinedRemoteJWKSet.js @@ -0,0 +1,55 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getCombinedJWKS = exports.resetCombinedJWKS = void 0; +const jose_1 = require("jose"); +const constants_1 = require("./recipe/session/constants"); +const querier_1 = require("./querier"); +let combinedJWKS; +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +function resetCombinedJWKS() { + combinedJWKS = undefined; +} +exports.resetCombinedJWKS = resetCombinedJWKS; +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +function getCombinedJWKS(config) { + if (combinedJWKS === undefined) { + const JWKS = querier_1.Querier.getNewInstanceOrThrowError(undefined) + .getAllCoreUrlsForPath("/.well-known/jwks.json") + .map((url) => + jose_1.createRemoteJWKSet(new URL(url), { + cacheMaxAge: config.jwksRefreshIntervalSec * 1000, + cooldownDuration: constants_1.JWKCacheCooldownInMs, + }) + ); + combinedJWKS = async (...args) => { + let lastError = undefined; + if (JWKS.length === 0) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + for (const jwks of JWKS) { + try { + // We await before returning to make sure we catch the error + return await jwks(...args); + } catch (ex) { + lastError = ex; + } + } + throw lastError; + }; + } + return combinedJWKS; +} +exports.getCombinedJWKS = getCombinedJWKS; diff --git a/lib/build/framework/request.d.ts b/lib/build/framework/request.d.ts index fafdcdb20..00c5fe6c3 100644 --- a/lib/build/framework/request.d.ts +++ b/lib/build/framework/request.d.ts @@ -15,4 +15,5 @@ export declare abstract class BaseRequest { abstract getOriginalURL: () => string; getFormData: () => Promise; getJSONBody: () => Promise; + getBodyAsJSONOrFormData: () => Promise; } diff --git a/lib/build/framework/request.js b/lib/build/framework/request.js index afc940e7a..edaa6e780 100644 --- a/lib/build/framework/request.js +++ b/lib/build/framework/request.js @@ -41,6 +41,26 @@ class BaseRequest { } return this.parsedJSONBody; }; + this.getBodyAsJSONOrFormData = async () => { + const contentType = this.getHeaderValue("content-type"); + if (contentType) { + if (contentType.startsWith("application/json")) { + return await this.getJSONBody(); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + return await this.getFormData(); + } + } else { + try { + return await this.getJSONBody(); + } catch (_a) { + try { + return await this.getFormData(); + } catch (_b) { + throw new Error("Unable to parse body as JSON or Form Data."); + } + } + } + }; this.wrapperUsed = true; this.parsedJSONBody = undefined; this.parsedUrlEncodedFormData = undefined; diff --git a/lib/build/querier.d.ts b/lib/build/querier.d.ts index dbc232eea..17d12763b 100644 --- a/lib/build/querier.d.ts +++ b/lib/build/querier.d.ts @@ -44,12 +44,19 @@ export declare class Querier { sendGetRequestWithResponseHeaders: ( path: NormalisedURLPath, params: Record, + inpHeaders: Record | undefined, userContext: UserContext ) => Promise<{ body: any; headers: Headers; }>; - sendPutRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; + sendPutRequest: ( + path: NormalisedURLPath, + body: any, + params: Record, + userContext: UserContext + ) => Promise; + sendPatchRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; invalidateCoreCallCache: (userContext: UserContext, updGlobalCacheTagIfNecessary?: boolean) => void; getAllCoreUrlsForPath(path: string): string[]; private sendRequestHelper; diff --git a/lib/build/querier.js b/lib/build/querier.js index 7803b6cc6..9ee1f364f 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -108,8 +108,8 @@ class Querier { let apiVersion = await this.getAPIVersion(userContext); let headers = { "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", }; + headers["content-type"] = "application/json; charset=utf-8"; if (Querier.apiKey !== undefined) { headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); } @@ -267,6 +267,9 @@ class Querier { method: "GET", headers, }); + if (response.status === 302) { + return response; + } if (response.status === 200 && !Querier.disableCache) { // If the request was successful, we save the result into the cache // plus we update the cache tag @@ -287,14 +290,15 @@ class Querier { ); return respBody; }; - this.sendGetRequestWithResponseHeaders = async (path, params, userContext) => { + this.sendGetRequestWithResponseHeaders = async (path, params, inpHeaders, userContext) => { var _a; return await this.sendRequestHelper( path, "GET", async (url) => { let apiVersion = await this.getAPIVersion(userContext); - let headers = { "cdi-version": apiVersion }; + let headers = inpHeaders !== null && inpHeaders !== void 0 ? inpHeaders : {}; + headers["cdi-version"] = apiVersion; if (Querier.apiKey !== undefined) { headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); } @@ -331,7 +335,7 @@ class Querier { ); }; // path should start with "/" - this.sendPutRequest = async (path, body, userContext) => { + this.sendPutRequest = async (path, body, params, userContext) => { var _a; this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( @@ -353,6 +357,7 @@ class Querier { method: "put", headers: headers, body: body, + params: params, }, userContext ); @@ -362,7 +367,12 @@ class Querier { body = request.body; } } - return utils_1.doFetch(url, { + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) + ); + finalURL.search = searchParams.toString(); + return utils_1.doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -372,6 +382,48 @@ class Querier { ); return respBody; }; + // path should start with "/" + this.sendPatchRequest = async (path, body, userContext) => { + var _a; + this.invalidateCoreCallCache(userContext); + const { body: respBody } = await this.sendRequestHelper( + path, + "PATCH", + async (url) => { + let apiVersion = await this.getAPIVersion(userContext); + let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + if (Querier.networkInterceptor !== undefined) { + let request = Querier.networkInterceptor( + { + url: url, + method: "patch", + headers: headers, + body: body, + }, + userContext + ); + url = request.url; + headers = request.headers; + if (request.body !== undefined) { + body = request.body; + } + } + return utils_1.doFetch(url, { + method: "PATCH", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; this.invalidateCoreCallCache = (userContext, updGlobalCacheTagIfNecessary = true) => { var _a; if ( @@ -395,7 +447,8 @@ class Querier { } let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - const url = currentDomain + currentBasePath + path.getAsStringDangerous(); + let strPath = path.getAsStringDangerous(); + const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; if (retryInfoMap === undefined) { retryInfoMap = {}; diff --git a/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js b/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js index 14d3a7313..e4f151e0d 100644 --- a/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js +++ b/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js @@ -9,9 +9,9 @@ const multitenancy_1 = __importDefault(require("../../../multitenancy")); const recipe_1 = __importDefault(require("../../../multitenancy/recipe")); const normalisedURLDomain_1 = __importDefault(require("../../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const utils_1 = require("../../../thirdparty/providers/utils"); +const thirdpartyUtils_1 = require("../../../../thirdpartyUtils"); const constants_1 = require("../../../multitenancy/constants"); -const utils_2 = require("../../../../utils"); +const utils_1 = require("../../../../utils"); async function createOrUpdateThirdPartyConfig(_, tenantId, options, userContext) { var _a; const requestBody = await options.req.getJSONBody(); @@ -66,7 +66,7 @@ async function createOrUpdateThirdPartyConfig(_, tenantId, options, userContext) defaultRedirectUrl: providerConfig.clients[0].additionalConfig.redirectURLs[0], forceAuthn: false, encodedRawMetadata: providerConfig.clients[0].additionalConfig.samlXML - ? utils_2.encodeBase64(providerConfig.clients[0].additionalConfig.samlXML) + ? utils_1.encodeBase64(providerConfig.clients[0].additionalConfig.samlXML) : "", redirectUrl: JSON.stringify(providerConfig.clients[0].additionalConfig.redirectURLs), metadataUrl: providerConfig.clients[0].additionalConfig.samlURL || "", @@ -74,7 +74,7 @@ async function createOrUpdateThirdPartyConfig(_, tenantId, options, userContext) const normalisedDomain = new normalisedURLDomain_1.default(boxyURL); const normalisedBasePath = new normalisedURLPath_1.default(boxyURL); const connectionsPath = new normalisedURLPath_1.default("/api/v1/saml/config"); - const resp = await utils_1.doPostRequest( + const resp = await thirdpartyUtils_1.doPostRequest( normalisedDomain.getAsStringDangerous() + normalisedBasePath.appendPath(connectionsPath).getAsStringDangerous(), requestBody, diff --git a/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js b/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js index f5b6c69d5..91278ce5f 100644 --- a/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js +++ b/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js @@ -21,7 +21,7 @@ const recipe_1 = __importDefault(require("../../../multitenancy/recipe")); const configUtils_1 = require("../../../thirdparty/providers/configUtils"); const normalisedURLDomain_1 = __importDefault(require("../../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const utils_1 = require("../../../thirdparty/providers/utils"); +const thirdpartyUtils_1 = require("../../../../thirdpartyUtils"); async function getThirdPartyConfig(_, tenantId, options, userContext) { var _a, _b, _c, _d, _e; let tenantRes = await multitenancy_1.default.getTenant(tenantId, userContext); @@ -246,7 +246,7 @@ async function getThirdPartyConfig(_, tenantId, options, userContext) { const normalisedDomain = new normalisedURLDomain_1.default(boxyURL); const normalisedBasePath = new normalisedURLPath_1.default(boxyURL); const connectionsPath = new normalisedURLPath_1.default("/api/v1/saml/config"); - const resp = await utils_1.doGetRequest( + const resp = await thirdpartyUtils_1.doGetRequest( normalisedDomain.getAsStringDangerous() + normalisedBasePath.appendPath(connectionsPath).getAsStringDangerous(), { diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 703c9462a..6619c14cf 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -40,7 +40,15 @@ function getAPIImplementation() { }; }, generatePasswordResetTokenPOST: async function ({ formFields, tenantId, options, userContext }) { - const email = formFields.filter((f) => f.id === "email")[0].value; + // NOTE: Check for email being a non-string value. This check will likely + // never evaluate to `true` as there is an upper-level check for the type + // in validation but kept here to be safe. + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + const email = emailAsUnknown; // this function will be reused in different parts of the flow below.. async function generateAndSendPasswordResetToken(primaryUserId, recipeUserId) { // the user ID here can be primary or recipe level. @@ -355,7 +363,15 @@ function getAPIImplementation() { }; } } - let newPassword = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Check for password being a non-string value. This check will likely + // never evaluate to `true` as there is an upper-level check for the type + // in validation but kept here to be safe. + const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + if (typeof newPasswordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + let newPassword = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, tenantId, @@ -371,7 +387,7 @@ function getAPIImplementation() { // This should happen only cause of a race condition where the user // might be deleted before token creation and consumption. // Also note that this being undefined doesn't mean that the email password - // user does not exist, but it means that there is no reicpe or primary user + // user does not exist, but it means that there is no recipe or primary user // for whom the token was generated. return { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", @@ -481,7 +497,14 @@ function getAPIImplementation() { ); } }, - signInPOST: async function ({ formFields, tenantId, session, options, userContext }) { + signInPOST: async function ({ + formFields, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { const errorCodeMap = { SIGN_IN_NOT_ALLOWED: "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", @@ -496,8 +519,21 @@ function getAPIImplementation() { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_012)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + if (typeof passwordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + let email = emailAsUnknown; + let password = passwordAsUnknown; const recipeId = "emailpassword"; const checkCredentialsOnTenant = async (tenantId) => { return ( @@ -546,6 +582,7 @@ function getAPIImplementation() { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status === "SIGN_UP_NOT_ALLOWED") { throw new Error("This should never happen: pre-auth checks should not fail for sign in"); @@ -567,6 +604,7 @@ function getAPIImplementation() { email, password, session, + shouldTryLinkingWithSessionUser, tenantId, userContext, }); @@ -604,7 +642,14 @@ function getAPIImplementation() { user: postAuthChecks.user, }; }, - signUpPOST: async function ({ formFields, tenantId, session, options, userContext }) { + signUpPOST: async function ({ + formFields, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { const errorCodeMap = { SIGN_UP_NOT_ALLOWED: "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", @@ -619,8 +664,21 @@ function getAPIImplementation() { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_016)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + if (typeof passwordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + let email = emailAsUnknown; + let password = passwordAsUnknown; const preAuthCheckRes = await authUtils_1.AuthUtils.preAuthChecks({ authenticatingAccountInfo: { recipeId: "emailpassword", @@ -635,6 +693,7 @@ function getAPIImplementation() { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthCheckRes.status === "SIGN_UP_NOT_ALLOWED") { const conflictingUsers = await recipe_1.default @@ -675,6 +734,7 @@ function getAPIImplementation() { email, password, session, + shouldTryLinkingWithSessionUser, userContext, }); if (signUpResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { diff --git a/lib/build/recipe/emailpassword/api/signin.js b/lib/build/recipe/emailpassword/api/signin.js index 0ddb015d1..42a7f16b3 100644 --- a/lib/build/recipe/emailpassword/api/signin.js +++ b/lib/build/recipe/emailpassword/api/signin.js @@ -13,34 +13,28 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signInAPI(apiImplementation, tenantId, options, userContext) { // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 if (apiImplementation.signInPOST === undefined) { return false; } + const body = await options.req.getJSONBody(); // step 1 let formFields = await utils_2.validateFormFieldsOrThrowError( options.config.signInFeature.formFields, - (await options.req.getJSONBody()).formFields, + body.formFields, tenantId, userContext ); - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -50,6 +44,7 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/emailpassword/api/signup.js b/lib/build/recipe/emailpassword/api/signup.js index 0987b15fb..6a793ec2c 100644 --- a/lib/build/recipe/emailpassword/api/signup.js +++ b/lib/build/recipe/emailpassword/api/signup.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signUpAPI(apiImplementation, tenantId, options, userContext) { // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 if (apiImplementation.signUpPOST === undefined) { @@ -36,13 +36,14 @@ async function signUpAPI(apiImplementation, tenantId, options, userContext) { tenantId, userContext ); - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + requestBody + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -52,6 +53,7 @@ async function signUpAPI(apiImplementation, tenantId, options, userContext) { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext: userContext, }); diff --git a/lib/build/recipe/emailpassword/api/utils.d.ts b/lib/build/recipe/emailpassword/api/utils.d.ts index d797f42e3..0f67df755 100644 --- a/lib/build/recipe/emailpassword/api/utils.d.ts +++ b/lib/build/recipe/emailpassword/api/utils.d.ts @@ -9,6 +9,6 @@ export declare function validateFormFieldsOrThrowError( ): Promise< { id: string; - value: string; + value: unknown; }[] >; diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js index afbca42de..69da81aba 100644 --- a/lib/build/recipe/emailpassword/api/utils.js +++ b/lib/build/recipe/emailpassword/api/utils.js @@ -27,7 +27,7 @@ async function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, t } if (curr.id === constants_1.FORM_FIELD_EMAIL_ID || curr.id === constants_1.FORM_FIELD_PASSWORD_ID) { if (typeof curr.value !== "string") { - throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); + throw newBadRequestError(`${curr.id} value must be a string`); } } formFields.push(curr); @@ -35,7 +35,9 @@ async function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, t // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 formFields = formFields.map((field) => { if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { - return Object.assign(Object.assign({}, field), { value: field.value.trim() }); + return Object.assign(Object.assign({}, field), { + value: typeof field.value === "string" ? field.value.trim() : field.value, + }); } return field; }); diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js index 7fcbacf41..2c77224c0 100644 --- a/lib/build/recipe/emailpassword/index.js +++ b/lib/build/recipe/emailpassword/index.js @@ -33,6 +33,7 @@ class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: utils_2.getUserContext(userContext), }); @@ -42,6 +43,7 @@ class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: utils_2.getUserContext(userContext), }); diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js index 8c09609c1..acbbb1c7f 100644 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ b/lib/build/recipe/emailpassword/recipeImplementation.js @@ -16,7 +16,7 @@ const user_1 = require("../../user"); const authUtils_1 = require("../../authUtils"); function getRecipeInterface(querier, getEmailPasswordConfig) { return { - signUp: async function ({ email, password, tenantId, session, userContext }) { + signUp: async function ({ email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext }) { const response = await this.createNewRecipeUser({ email, password, @@ -27,12 +27,13 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { return response; } let updatedUser = response.user; - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, } ); @@ -68,7 +69,7 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { // we do not do email verification here cause it's a new user and email password // users are always initially unverified. }, - signIn: async function ({ email, password, tenantId, session, userContext }) { + signIn: async function ({ email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext }) { const response = await this.verifyCredentials({ email, password, tenantId, userContext }); if (response.status === "OK") { const loginMethod = response.user.loginMethods.find( @@ -93,12 +94,13 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { // function updated the verification status) and can return that response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); } - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, } ); @@ -223,6 +225,7 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { email: input.email, password: input.password, }, + {}, input.userContext ); if (response.status === "OK") { diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index 58be90676..fa3eb1408 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -67,6 +67,7 @@ export declare type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -106,6 +107,7 @@ export declare type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -225,7 +227,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; options: APIOptions; @@ -245,7 +247,7 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -271,10 +273,11 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< @@ -297,10 +300,11 @@ export declare type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< diff --git a/lib/build/recipe/jwt/recipeImplementation.js b/lib/build/recipe/jwt/recipeImplementation.js index 073f14b88..0ac8ddf6f 100644 --- a/lib/build/recipe/jwt/recipeImplementation.js +++ b/lib/build/recipe/jwt/recipeImplementation.js @@ -54,6 +54,7 @@ function getRecipeInterface(querier, config, appInfo) { const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new normalisedURLPath_1.default("/.well-known/jwks.json"), {}, + undefined, userContext ); let validityInSeconds = defaultJWKSMaxAge; diff --git a/lib/build/recipe/multitenancy/recipeImplementation.js b/lib/build/recipe/multitenancy/recipeImplementation.js index 2ccd42009..044c7e319 100644 --- a/lib/build/recipe/multitenancy/recipeImplementation.js +++ b/lib/build/recipe/multitenancy/recipeImplementation.js @@ -16,6 +16,7 @@ function getRecipeInterface(querier) { let response = await querier.sendPutRequest( new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/v2`), Object.assign({ tenantId }, config), + {}, userContext ); return response; @@ -64,6 +65,7 @@ function getRecipeInterface(querier) { config, skipValidation, }, + {}, userContext ); return response; diff --git a/lib/build/recipe/oauth2client/api/implementation.d.ts b/lib/build/recipe/oauth2client/api/implementation.d.ts new file mode 100644 index 000000000..dd40e7025 --- /dev/null +++ b/lib/build/recipe/oauth2client/api/implementation.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { APIInterface } from "../"; +export default function getAPIInterface(): APIInterface; diff --git a/lib/build/recipe/oauth2client/api/implementation.js b/lib/build/recipe/oauth2client/api/implementation.js new file mode 100644 index 000000000..5a5e1d56f --- /dev/null +++ b/lib/build/recipe/oauth2client/api/implementation.js @@ -0,0 +1,69 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const session_1 = __importDefault(require("../../session")); +function getAPIInterface() { + return { + signInPOST: async function (input) { + const { options, tenantId, userContext, clientId } = input; + let normalisedClientId = clientId; + if (normalisedClientId === undefined) { + if (options.config.providerConfigs.length > 1) { + throw new Error( + "Should never come here: clientId is undefined and there are multiple providerConfigs" + ); + } + normalisedClientId = options.config.providerConfigs[0].clientId; + } + const providerConfig = await options.recipeImplementation.getProviderConfig({ + clientId: normalisedClientId, + userContext, + }); + let oAuthTokensToUse = {}; + if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { + oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo: input.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { + oAuthTokensToUse = input.oAuthTokens; + } else { + throw Error("should never come here"); + } + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ + providerConfig, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + const { user, recipeUserId } = await options.recipeImplementation.signIn({ + userId, + tenantId, + rawUserInfo, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + const session = await session_1.default.createNewSession( + options.req, + options.res, + tenantId, + recipeUserId, + undefined, + undefined, + userContext + ); + return { + status: "OK", + user, + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfo, + }; + }, + }; +} +exports.default = getAPIInterface; diff --git a/lib/build/recipe/oauth2client/api/signin.d.ts b/lib/build/recipe/oauth2client/api/signin.d.ts new file mode 100644 index 000000000..72cd6e46b --- /dev/null +++ b/lib/build/recipe/oauth2client/api/signin.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function signInAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2client/api/signin.js b/lib/build/recipe/oauth2client/api/signin.js new file mode 100644 index 000000000..57155dc81 --- /dev/null +++ b/lib/build/recipe/oauth2client/api/signin.js @@ -0,0 +1,74 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const error_1 = __importDefault(require("../../../error")); +const utils_1 = require("../../../utils"); +async function signInAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signInPOST === undefined) { + return false; + } + const bodyParams = await options.req.getJSONBody(); + let redirectURIInfo; + let oAuthTokens; + if (bodyParams.clientId === undefined && options.config.providerConfigs.length > 1) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the clientId in request body", + }); + } + if (bodyParams.redirectURIInfo !== undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the redirectURI in request body", + }); + } + redirectURIInfo = bodyParams.redirectURIInfo; + } else if (bodyParams.oAuthTokens !== undefined) { + oAuthTokens = bodyParams.oAuthTokens; + } else { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", + }); + } + let result = await apiImplementation.signInPOST({ + tenantId, + clientId: bodyParams.clientId, + redirectURIInfo, + oAuthTokens, + options, + userContext, + }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign( + { status: result.status }, + utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext) + ) + ); + } else { + utils_1.send200Response(options.res, result); + } + return true; +} +exports.default = signInAPI; diff --git a/lib/build/recipe/oauth2client/constants.d.ts b/lib/build/recipe/oauth2client/constants.d.ts new file mode 100644 index 000000000..1fb91e760 --- /dev/null +++ b/lib/build/recipe/oauth2client/constants.d.ts @@ -0,0 +1,2 @@ +// @ts-nocheck +export declare const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/constants.js b/lib/build/recipe/oauth2client/constants.js new file mode 100644 index 000000000..4f42a6cb4 --- /dev/null +++ b/lib/build/recipe/oauth2client/constants.js @@ -0,0 +1,18 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SIGN_IN_API = void 0; +exports.SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/index.d.ts b/lib/build/recipe/oauth2client/index.d.ts new file mode 100644 index 000000000..ff01c3827 --- /dev/null +++ b/lib/build/recipe/oauth2client/index.d.ts @@ -0,0 +1,23 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; +export default class Wrapper { + static init: typeof Recipe.init; + static exchangeAuthCodeForOAuthTokens( + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }, + clientId?: string, + userContext?: Record + ): Promise; + static getUserInfo( + oAuthTokens: OAuthTokens, + userContext?: Record + ): Promise; +} +export declare let init: typeof Recipe.init; +export declare let exchangeAuthCodeForOAuthTokens: typeof Wrapper.exchangeAuthCodeForOAuthTokens; +export declare let getUserInfo: typeof Wrapper.getUserInfo; +export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/build/recipe/oauth2client/index.js b/lib/build/recipe/oauth2client/index.js new file mode 100644 index 000000000..e24e2d323 --- /dev/null +++ b/lib/build/recipe/oauth2client/index.js @@ -0,0 +1,70 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.init = void 0; +const utils_1 = require("../../utils"); +const jwt_1 = require("../session/jwt"); +const recipe_1 = __importDefault(require("./recipe")); +class Wrapper { + static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, clientId, userContext) { + let normalisedClientId = clientId; + const instance = recipe_1.default.getInstanceOrThrowError(); + const recipeInterfaceImpl = instance.recipeInterfaceImpl; + const normalisedUserContext = utils_1.getUserContext(userContext); + if (normalisedClientId === undefined) { + if (instance.config.providerConfigs.length > 1) { + throw new Error("clientId is required if there are more than one provider configs defined"); + } + normalisedClientId = instance.config.providerConfigs[0].clientId; + } + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + clientId: normalisedClientId, + userContext: normalisedUserContext, + }); + return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo, + userContext: normalisedUserContext, + }); + } + static async getUserInfo(oAuthTokens, userContext) { + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = utils_1.getUserContext(userContext); + if (oAuthTokens.access_token === undefined) { + throw new Error("access_token is required to get user info"); + } + const preparseJWTInfo = jwt_1.parseJWTWithoutSignatureVerification(oAuthTokens.access_token); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + clientId: preparseJWTInfo.payload.client_id, + userContext: normalisedUserContext, + }); + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ + providerConfig, + oAuthTokens, + userContext: normalisedUserContext, + }); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +exports.init = Wrapper.init; +exports.exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; +exports.getUserInfo = Wrapper.getUserInfo; diff --git a/lib/build/recipe/oauth2client/recipe.d.ts b/lib/build/recipe/oauth2client/recipe.d.ts new file mode 100644 index 000000000..180227169 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipe.d.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +import RecipeModule from "../../recipeModule"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import STError from "../../error"; +import NormalisedURLPath from "../../normalisedURLPath"; +import type { BaseRequest, BaseResponse } from "../../framework"; +export default class Recipe extends RecipeModule { + private static instance; + static RECIPE_ID: string; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + _recipes: {} + ); + static init(config: TypeInput): RecipeListFunction; + static getInstanceOrThrowError(): Recipe; + static reset(): void; + getAPIsHandled: () => APIHandled[]; + handleAPIRequest: ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ) => Promise; + handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; + getAllCORSHeaders: () => string[]; + isErrorFromThisRecipe: (err: any) => err is STError; +} diff --git a/lib/build/recipe/oauth2client/recipe.js b/lib/build/recipe/oauth2client/recipe.js new file mode 100644 index 000000000..daf6b07f3 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipe.js @@ -0,0 +1,107 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipeModule_1 = __importDefault(require("../../recipeModule")); +const utils_1 = require("./utils"); +const error_1 = __importDefault(require("../../error")); +const constants_1 = require("./constants"); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const signin_1 = __importDefault(require("./api/signin")); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const implementation_1 = __importDefault(require("./api/implementation")); +const querier_1 = require("../../querier"); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, isInServerlessEnv, config, _recipes) { + super(recipeId, appInfo); + this.getAPIsHandled = () => { + return [ + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_API), + id: constants_1.SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + ]; + }; + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.SIGN_IN_API) { + return await signin_1.default(this.apiImpl, tenantId, options, userContext); + } + return false; + }; + this.handleError = async (err, _request, _response) => { + throw err; + }; + this.getAllCORSHeaders = () => { + return []; + }; + this.isErrorFromThisRecipe = (err) => { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + }; + this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + { + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId), this.config) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new supertokens_js_override_1.default(implementation_1.default()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + static init(config) { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); + return Recipe.instance; + } else { + throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); + } + }; + } + static getInstanceOrThrowError() { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); + } + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } +} +exports.default = Recipe; +Recipe.instance = undefined; +Recipe.RECIPE_ID = "oauth2client"; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.d.ts b/lib/build/recipe/oauth2client/recipeImplementation.d.ts new file mode 100644 index 000000000..24599a2c7 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipeImplementation.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +import { RecipeInterface, TypeNormalisedInput } from "./types"; +import { Querier } from "../../querier"; +export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js new file mode 100644 index 000000000..351fa3a45 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -0,0 +1,142 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const thirdpartyUtils_1 = require("../../thirdpartyUtils"); +const __1 = require("../.."); +const logger_1 = require("../../logger"); +const jose_1 = require("jose"); +function getRecipeImplementation(_querier, config) { + let providerConfigsWithOIDCInfo = {}; + return { + signIn: async function ({ userId, tenantId, userContext, oAuthTokens, rawUserInfo }) { + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); + } + return { + status: "OK", + user, + recipeUserId: new recipeUserId_1.default(userId), + oAuthTokens, + rawUserInfo, + }; + }, + getProviderConfig: async function ({ clientId }) { + if (providerConfigsWithOIDCInfo[clientId] !== undefined) { + return providerConfigsWithOIDCInfo[clientId]; + } + const providerConfig = config.providerConfigs.find( + (providerConfig) => providerConfig.clientId === clientId + ); + const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(providerConfig.oidcDiscoveryEndpoint); + if (oidcInfo.authorization_endpoint === undefined) { + throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.token_endpoint === undefined) { + throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.jwks_uri === undefined) { + throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); + } + providerConfigsWithOIDCInfo[clientId] = Object.assign(Object.assign({}, providerConfig), { + authorizationEndpoint: oidcInfo.authorization_endpoint, + tokenEndpoint: oidcInfo.token_endpoint, + userInfoEndpoint: oidcInfo.userinfo_endpoint, + jwksURI: oidcInfo.jwks_uri, + }); + return providerConfigsWithOIDCInfo[clientId]; + }, + exchangeAuthCodeForOAuthTokens: async function ({ providerConfig, redirectURIInfo }) { + if (providerConfig.tokenEndpoint === undefined) { + throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); + } + const tokenAPIURL = providerConfig.tokenEndpoint; + const accessTokenAPIParams = { + client_id: providerConfig.clientId, + redirect_uri: redirectURIInfo.redirectURI, + code: redirectURIInfo.redirectURIQueryParams["code"], + grant_type: "authorization_code", + }; + if (providerConfig.clientSecret !== undefined) { + accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; + } + if (redirectURIInfo.pkceCodeVerifier !== undefined) { + accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + const tokenResponse = await thirdpartyUtils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); + if (tokenResponse.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + throw new Error( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + } + return tokenResponse.jsonResponse; + }, + getUserInfo: async function ({ providerConfig, oAuthTokens }) { + var _a, _b; + let jwks; + const accessToken = oAuthTokens["access_token"]; + const idToken = oAuthTokens["id_token"]; + let rawUserInfo = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + if (idToken && providerConfig.jwksURI !== undefined) { + if (jwks === undefined) { + jwks = jose_1.createRemoteJWKSet(new URL(providerConfig.jwksURI)); + } + rawUserInfo.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken, + jwks, + { + audience: providerConfig.clientId, + } + ); + } + if (accessToken && providerConfig.userInfoEndpoint !== undefined) { + const headers = { + Authorization: "Bearer " + accessToken, + }; + const queryParams = {}; + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( + providerConfig.userInfoEndpoint, + queryParams, + headers + ); + if (userInfoFromAccessToken.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + throw new Error( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + } + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + } + let userId = undefined; + if (((_a = rawUserInfo.fromIdTokenPayload) === null || _a === void 0 ? void 0 : _a.sub) !== undefined) { + userId = rawUserInfo.fromIdTokenPayload["sub"]; + } else if (((_b = rawUserInfo.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.sub) !== undefined) { + userId = rawUserInfo.fromUserInfoAPI["sub"]; + } + if (userId === undefined) { + throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); + } + return { + userId, + rawUserInfo, + }; + }, + }; +} +exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/oauth2client/types.d.ts b/lib/build/recipe/oauth2client/types.d.ts new file mode 100644 index 000000000..84fab1847 --- /dev/null +++ b/lib/build/recipe/oauth2client/types.d.ts @@ -0,0 +1,155 @@ +// @ts-nocheck +import type { BaseRequest, BaseResponse } from "../../framework"; +import { NormalisedAppinfo, UserContext } from "../../types"; +import OverrideableBuilder from "supertokens-js-override"; +import { SessionContainerInterface } from "../session/types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; +export declare type UserInfo = { + userId: string; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; +}; +export declare type ProviderConfigInput = { + clientId: string; + clientSecret?: string; + oidcDiscoveryEndpoint: string; +}; +export declare type ProviderConfigWithOIDCInfo = ProviderConfigInput & { + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; + jwksURI: string; +}; +export declare type OAuthTokens = { + access_token?: string; + id_token?: string; +}; +export declare type OAuthTokenResponse = { + access_token: string; + id_token?: string; + refresh_token?: string; + expires_in: number; + scope?: string; + token_type: string; +}; +export declare type TypeInput = { + providerConfigs: ProviderConfigInput[]; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeNormalisedInput = { + providerConfigs: ProviderConfigInput[]; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type RecipeInterface = { + getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; + signIn(input: { + userId: string; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + tenantId: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + }>; + exchangeAuthCodeForOAuthTokens(input: { + providerConfig: ProviderConfigWithOIDCInfo; + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }; + userContext: UserContext; + }): Promise; + getUserInfo(input: { + providerConfig: ProviderConfigWithOIDCInfo; + oAuthTokens: OAuthTokens; + userContext: UserContext; + }): Promise; +}; +export declare type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; + appInfo: NormalisedAppinfo; +}; +export declare type APIInterface = { + signInPOST: ( + input: { + tenantId: string; + clientId?: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + } + | { + oAuthTokens: { + [key: string]: any; + }; + } + ) + ) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + oAuthTokens: { + [key: string]: any; + }; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + } + | GeneralErrorResponse + >; +}; diff --git a/lib/build/recipe/oauth2client/types.js b/lib/build/recipe/oauth2client/types.js new file mode 100644 index 000000000..9f1237319 --- /dev/null +++ b/lib/build/recipe/oauth2client/types.js @@ -0,0 +1,16 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/oauth2client/utils.d.ts b/lib/build/recipe/oauth2client/utils.d.ts new file mode 100644 index 000000000..6a930e641 --- /dev/null +++ b/lib/build/recipe/oauth2client/utils.d.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import { NormalisedAppinfo } from "../../types"; +import { TypeInput, TypeNormalisedInput } from "./types"; +export declare function validateAndNormaliseUserInput( + _appInfo: NormalisedAppinfo, + config: TypeInput +): TypeNormalisedInput; diff --git a/lib/build/recipe/oauth2client/utils.js b/lib/build/recipe/oauth2client/utils.js new file mode 100644 index 000000000..25e759254 --- /dev/null +++ b/lib/build/recipe/oauth2client/utils.js @@ -0,0 +1,45 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateAndNormaliseUserInput = void 0; +function validateAndNormaliseUserInput(_appInfo, config) { + if (config === undefined || config.providerConfigs === undefined) { + throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); + } + if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { + throw new Error("Please pass clientId for all providerConfigs."); + } + if (!config.providerConfigs.every((providerConfig) => providerConfig.clientId.startsWith("stcl_"))) { + throw new Error( + `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the ThirdParty recipe.` + ); + } + if (config.providerConfigs.some((providerConfig) => providerConfig.oidcDiscoveryEndpoint === undefined)) { + throw new Error("Please pass oidcDiscoveryEndpoint for all providerConfigs."); + } + let override = Object.assign( + { + functions: (originalImplementation) => originalImplementation, + apis: (originalImplementation) => originalImplementation, + }, + config === null || config === void 0 ? void 0 : config.override + ); + return { + providerConfigs: config.providerConfigs, + override, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts new file mode 100644 index 000000000..7ffba30b1 --- /dev/null +++ b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts @@ -0,0 +1,171 @@ +// @ts-nocheck +import { OAuth2ClientOptions } from "./types"; +export declare class OAuth2Client { + /** + * OAuth 2.0 Client ID + * The ID is immutable. If no ID is provided, a UUID4 will be generated. + */ + clientId: string; + /** + * OAuth 2.0 Client Secret + * The secret will be included in the create request as cleartext, and then + * never again. The secret is kept in hashed format and is not recoverable once lost. + */ + clientSecret?: string; + /** + * OAuth 2.0 Client Name + * The human-readable name of the client to be presented to the end-user during authorization. + */ + clientName: string; + /** + * OAuth 2.0 Client Scope + * Scope is a string containing a space-separated list of scope values that the client + * can use when requesting access tokens. + */ + scope: string; + /** + * Array of redirect URIs + */ + redirectUris: string[] | null; + /** + * Array of post logout redirect URIs + * + * This field holds a list of whitelisted `post_logout_redirect_uri`s used to redirect the user after + * logout via the `end_session_endpoint`. If a non-whitelisted URI is provided, the logout request is rejected. + * + * By default, this field is absent in the OAuth2Client. If provided, it must be a non-empty array of strings, + * with each URI’s domain, port, and scheme matching at least one registered redirect URI. + */ + postLogoutRedirectUris?: string[]; + /** + * Authorization Code Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantAccessTokenLifespan: string | null; + /** + * Authorization Code Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantIdTokenLifespan: string | null; + /** + * Authorization Code Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantRefreshTokenLifespan: string | null; + /** + * Client Credentials Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + clientCredentialsGrantAccessTokenLifespan: string | null; + /** + * Implicit Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantAccessTokenLifespan: string | null; + /** + * Implicit Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantIdTokenLifespan: string | null; + /** + * Refresh Token Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantAccessTokenLifespan: string | null; + /** + * Refresh Token Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantIdTokenLifespan: string | null; + /** + * Refresh Token Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantRefreshTokenLifespan: string | null; + /** + * OAuth 2.0 Token Endpoint Authentication Method + * Requested Client Authentication method for the Token Endpoint. + */ + tokenEndpointAuthMethod: string; + /** + * OAuth 2.0 Client URI + * ClientURI is a URL string of a web page providing information about the client. + */ + clientUri: string; + /** + * Array of audiences + */ + audience: string[]; + /** + * Array of grant types + */ + grantTypes: string[] | null; + /** + * Array of response types + */ + responseTypes: string[] | null; + /** + * OAuth 2.0 Client Logo URI + * A URL string referencing the client's logo. + */ + logoUri: string; + /** + * OAuth 2.0 Client Policy URI + * PolicyURI is a URL string that points to a human-readable privacy policy document + * that describes how the deployment organization collects, uses, + * retains, and discloses personal data. + */ + policyUri: string; + /** + * OAuth 2.0 Client Terms of Service URI + * A URL string pointing to a human-readable terms of service + * document for the client that describes a contractual relationship + * between the end-user and the client that the end-user accepts when + * authorizing the client. + */ + tosUri: string; + /** + * OAuth 2.0 Client Creation Date + * CreatedAt returns the timestamp of the client's creation. + */ + createdAt: string; + /** + * OAuth 2.0 Client Last Update Date + * UpdatedAt returns the timestamp of the last update. + */ + updatedAt: string; + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + metadata: Record; + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris, + postLogoutRedirectUris, + authorizationCodeGrantAccessTokenLifespan, + authorizationCodeGrantIdTokenLifespan, + authorizationCodeGrantRefreshTokenLifespan, + clientCredentialsGrantAccessTokenLifespan, + implicitGrantAccessTokenLifespan, + implicitGrantIdTokenLifespan, + refreshTokenGrantAccessTokenLifespan, + refreshTokenGrantIdTokenLifespan, + refreshTokenGrantRefreshTokenLifespan, + tokenEndpointAuthMethod, + clientUri, + audience, + grantTypes, + responseTypes, + logoUri, + policyUri, + tosUri, + createdAt, + updatedAt, + metadata, + }: OAuth2ClientOptions); + static fromAPIResponse(response: any): OAuth2Client; +} diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.js b/lib/build/recipe/oauth2provider/OAuth2Client.js new file mode 100644 index 000000000..4c700f04f --- /dev/null +++ b/lib/build/recipe/oauth2provider/OAuth2Client.js @@ -0,0 +1,84 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OAuth2Client = void 0; +const utils_1 = require("../../utils"); +class OAuth2Client { + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris = null, + postLogoutRedirectUris, + authorizationCodeGrantAccessTokenLifespan = null, + authorizationCodeGrantIdTokenLifespan = null, + authorizationCodeGrantRefreshTokenLifespan = null, + clientCredentialsGrantAccessTokenLifespan = null, + implicitGrantAccessTokenLifespan = null, + implicitGrantIdTokenLifespan = null, + refreshTokenGrantAccessTokenLifespan = null, + refreshTokenGrantIdTokenLifespan = null, + refreshTokenGrantRefreshTokenLifespan = null, + tokenEndpointAuthMethod, + clientUri = "", + audience = [], + grantTypes = null, + responseTypes = null, + logoUri = "", + policyUri = "", + tosUri = "", + createdAt, + updatedAt, + metadata = {}, + }) { + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + this.metadata = {}; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.clientName = clientName; + this.scope = scope; + this.redirectUris = redirectUris; + this.postLogoutRedirectUris = postLogoutRedirectUris; + this.authorizationCodeGrantAccessTokenLifespan = authorizationCodeGrantAccessTokenLifespan; + this.authorizationCodeGrantIdTokenLifespan = authorizationCodeGrantIdTokenLifespan; + this.authorizationCodeGrantRefreshTokenLifespan = authorizationCodeGrantRefreshTokenLifespan; + this.clientCredentialsGrantAccessTokenLifespan = clientCredentialsGrantAccessTokenLifespan; + this.implicitGrantAccessTokenLifespan = implicitGrantAccessTokenLifespan; + this.implicitGrantIdTokenLifespan = implicitGrantIdTokenLifespan; + this.refreshTokenGrantAccessTokenLifespan = refreshTokenGrantAccessTokenLifespan; + this.refreshTokenGrantIdTokenLifespan = refreshTokenGrantIdTokenLifespan; + this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; + this.clientUri = clientUri; + this.audience = audience; + this.grantTypes = grantTypes; + this.responseTypes = responseTypes; + this.logoUri = logoUri; + this.policyUri = policyUri; + this.tosUri = tosUri; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.metadata = metadata; + } + static fromAPIResponse(response) { + return new OAuth2Client(utils_1.transformObjectKeys(response, "camelCase")); + } +} +exports.OAuth2Client = OAuth2Client; diff --git a/lib/build/recipe/oauth2provider/api/auth.d.ts b/lib/build/recipe/oauth2provider/api/auth.d.ts new file mode 100644 index 000000000..059876918 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/auth.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function authGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/auth.js b/lib/build/recipe/oauth2provider/api/auth.js new file mode 100644 index 000000000..b5c0e4afc --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/auth.js @@ -0,0 +1,84 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../recipe/session/error")); +async function authGET(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.authGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + let session, shouldTryRefresh; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + session = undefined; + if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_1.default.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + // This should generally not happen, but we can handle this as if the session is not present, + // because then we redirect to the frontend, which should handle the validation error + shouldTryRefresh = false; + } + } + let response = await apiImplementation.authGET({ + options, + params: Object.fromEntries(params.entries()), + cookie: options.req.getHeaderValue("cookie"), + session, + shouldTryRefresh, + userContext, + }); + if ("redirectTo" in response) { + if (response.cookies) { + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.cookies); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); + } + } + options.res.original.redirect(response.redirectTo); + } else if ("statusCode" in response) { + utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = authGET; diff --git a/lib/build/recipe/oauth2provider/api/endSession.d.ts b/lib/build/recipe/oauth2provider/api/endSession.d.ts new file mode 100644 index 000000000..1f454cbd0 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/endSession.d.ts @@ -0,0 +1,13 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export declare function endSessionGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; +export declare function endSessionPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/endSession.js b/lib/build/recipe/oauth2provider/api/endSession.js new file mode 100644 index 000000000..f0d1da3b2 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/endSession.js @@ -0,0 +1,87 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.endSessionPOST = exports.endSessionGET = void 0; +const utils_1 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../error")); +const error_2 = __importDefault(require("../../../recipe/session/error")); +async function endSessionGET(apiImplementation, options, userContext) { + if (apiImplementation.endSessionGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + return endSessionCommon( + Object.fromEntries(params.entries()), + apiImplementation.endSessionGET, + options, + userContext + ); +} +exports.endSessionGET = endSessionGET; +async function endSessionPOST(apiImplementation, options, userContext) { + if (apiImplementation.endSessionPOST === undefined) { + return false; + } + const params = await options.req.getBodyAsJSONOrFormData(); + return endSessionCommon(params, apiImplementation.endSessionPOST, options, userContext); +} +exports.endSessionPOST = endSessionPOST; +async function endSessionCommon(params, apiImplementation, options, userContext) { + var _a; + if (apiImplementation === undefined) { + return false; + } + let session, shouldTryRefresh; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_2.default.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + let response = await apiImplementation({ + options, + params, + session, + shouldTryRefresh, + userContext, + }); + if ("redirectTo" in response) { + options.res.original.redirect(response.redirectTo); + } else if ("error" in response) { + utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} diff --git a/lib/build/recipe/oauth2provider/api/implementation.d.ts b/lib/build/recipe/oauth2provider/api/implementation.d.ts new file mode 100644 index 000000000..0218549fa --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/implementation.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { APIInterface } from "../types"; +export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js new file mode 100644 index 000000000..fa4232d17 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -0,0 +1,184 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("./utils"); +function getAPIImplementation() { + return { + loginGET: async ({ loginChallenge, options, session, shouldTryRefresh, userContext }) => { + const response = await utils_1.loginGET({ + recipeImplementation: options.recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + isDirectCall: true, + userContext, + }); + if ("error" in response) { + return response; + } + const respAfterInternalRedirects = await utils_1.handleLoginInternalRedirects({ + response, + cookie: options.req.getHeaderValue("cookie"), + recipeImplementation: options.recipeImplementation, + session, + shouldTryRefresh, + userContext, + }); + if ("error" in respAfterInternalRedirects) { + return respAfterInternalRedirects; + } + return { + frontendRedirectTo: respAfterInternalRedirects.redirectTo, + cookies: respAfterInternalRedirects.cookies, + }; + }, + authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.authorization({ + params, + cookies: cookie, + session, + userContext, + }); + if ("error" in response) { + return response; + } + return utils_1.handleLoginInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + cookie, + session, + shouldTryRefresh, + userContext, + }); + }, + tokenPOST: async (input) => { + return input.options.recipeImplementation.tokenExchange({ + authorizationHeader: input.authorizationHeader, + body: input.body, + userContext: input.userContext, + }); + }, + loginInfoGET: async ({ loginChallenge, options, userContext }) => { + const loginRes = await options.recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + if (loginRes.status === "ERROR") { + return loginRes; + } + const { client } = loginRes; + return { + status: "OK", + info: { + clientId: client.clientId, + clientName: client.clientName, + tosUri: client.tosUri, + policyUri: client.policyUri, + logoUri: client.logoUri, + clientUri: client.clientUri, + metadata: client.metadata, + }, + }; + }, + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { + return options.recipeImplementation.buildUserInfo({ + user, + accessTokenPayload, + scopes, + tenantId, + userContext, + }); + }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else if ("clientId" in input && input.clientId !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } else { + throw new Error(`Either of 'authorizationHeader' or 'clientId' must be provided`); + } + }, + introspectTokenPOST: async (input) => { + return input.options.recipeImplementation.introspectToken({ + token: input.token, + scopes: input.scopes, + userContext: input.userContext, + }); + }, + endSessionGET: async ({ options, params, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.endSession({ + params, + session, + shouldTryRefresh, + userContext, + }); + if ("error" in response) { + return response; + } + return utils_1.handleLogoutInternalRedirects({ + response, + session, + recipeImplementation: options.recipeImplementation, + userContext, + }); + }, + endSessionPOST: async ({ options, params, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.endSession({ + params, + session, + shouldTryRefresh, + userContext, + }); + if ("error" in response) { + return response; + } + return utils_1.handleLogoutInternalRedirects({ + response, + session, + recipeImplementation: options.recipeImplementation, + userContext, + }); + }, + logoutPOST: async ({ logoutChallenge, options, session, userContext }) => { + if (session != undefined) { + await session.revokeSession(userContext); + } + const response = await options.recipeImplementation.acceptLogoutRequest({ + challenge: logoutChallenge, + userContext, + }); + const res = await utils_1.handleLogoutInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + userContext, + }); + if ("error" in res) { + return res; + } + return { status: "OK", frontendRedirectTo: res.redirectTo }; + }, + }; +} +exports.default = getAPIImplementation; diff --git a/lib/build/recipe/oauth2provider/api/introspectToken.d.ts b/lib/build/recipe/oauth2provider/api/introspectToken.d.ts new file mode 100644 index 000000000..3d2972c0d --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/introspectToken.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function introspectTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/introspectToken.js b/lib/build/recipe/oauth2provider/api/introspectToken.js new file mode 100644 index 000000000..6e36c9c68 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/introspectToken.js @@ -0,0 +1,37 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function introspectTokenPOST(apiImplementation, options, userContext) { + if (apiImplementation.introspectTokenPOST === undefined) { + return false; + } + const body = await options.req.getBodyAsJSONOrFormData(); + if (body.token === undefined) { + utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + const scopes = body.scope ? body.scope.split(" ") : []; + let response = await apiImplementation.introspectTokenPOST({ + options, + token: body.token, + scopes, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = introspectTokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/login.d.ts b/lib/build/recipe/oauth2provider/api/login.d.ts new file mode 100644 index 000000000..6f4253cef --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/login.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function login( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/login.js b/lib/build/recipe/oauth2provider/api/login.js new file mode 100644 index 000000000..186337139 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/login.js @@ -0,0 +1,93 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +const utils_1 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../error")); +const error_2 = __importDefault(require("../../../recipe/session/error")); +async function login(apiImplementation, options, userContext) { + var _a, _b; + if (apiImplementation.loginGET === undefined) { + return false; + } + let session, shouldTryRefresh; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_2.default.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + const loginChallenge = + (_a = options.req.getKeyValueFromQuery("login_challenge")) !== null && _a !== void 0 + ? _a + : options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginGET({ + options, + loginChallenge, + session, + shouldTryRefresh, + userContext, + }); + if ("frontendRedirectTo" in response) { + if (response.cookies) { + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.cookies); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); + } + } + utils_1.send200Response(options.res, { + frontendRedirectTo: response.frontendRedirectTo, + }); + } else if ("statusCode" in response) { + utils_1.sendNon200Response(options.res, (_b = response.statusCode) !== null && _b !== void 0 ? _b : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = login; diff --git a/lib/build/recipe/oauth2provider/api/loginInfo.d.ts b/lib/build/recipe/oauth2provider/api/loginInfo.d.ts new file mode 100644 index 000000000..536858263 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/loginInfo.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function loginInfoGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/loginInfo.js b/lib/build/recipe/oauth2provider/api/loginInfo.js new file mode 100644 index 000000000..15b9da808 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/loginInfo.js @@ -0,0 +1,47 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../../../error")); +async function loginInfoGET(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.loginInfoGET === undefined) { + return false; + } + const loginChallenge = + (_a = options.req.getKeyValueFromQuery("login_challenge")) !== null && _a !== void 0 + ? _a + : options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginInfoGET({ + options, + loginChallenge, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = loginInfoGET; diff --git a/lib/build/recipe/oauth2provider/api/logout.d.ts b/lib/build/recipe/oauth2provider/api/logout.d.ts new file mode 100644 index 000000000..339a73e77 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/logout.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export declare function logoutPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/logout.js b/lib/build/recipe/oauth2provider/api/logout.js new file mode 100644 index 000000000..16a51b511 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/logout.js @@ -0,0 +1,62 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logoutPOST = void 0; +const utils_1 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../error")); +async function logoutPOST(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.logoutPOST === undefined) { + return false; + } + let session; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + } catch (_b) { + session = undefined; + } + const body = await options.req.getBodyAsJSONOrFormData(); + if (body.logoutChallenge === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Missing body param: logoutChallenge", + }); + } + let response = await apiImplementation.logoutPOST({ + options, + logoutChallenge: body.logoutChallenge, + session, + userContext, + }); + if ("status" in response && response.status === "OK") { + utils_1.send200Response(options.res, response); + } else if ("statusCode" in response) { + utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.logoutPOST = logoutPOST; diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.d.ts b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts new file mode 100644 index 000000000..902e734d5 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function revokeTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.js b/lib/build/recipe/oauth2provider/api/revokeToken.js new file mode 100644 index 000000000..7a190ae5d --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/revokeToken.js @@ -0,0 +1,55 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function revokeTokenPOST(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + const body = await options.req.getBodyAsJSONOrFormData(); + if (body.token === undefined) { + utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + const authorizationHeader = options.req.getHeaderValue("authorization"); + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + utils_1.sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + if ("statusCode" in response && response.statusCode !== 200) { + utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = revokeTokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/token.d.ts b/lib/build/recipe/oauth2provider/api/token.d.ts new file mode 100644 index 000000000..c697b7744 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/token.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function tokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/token.js b/lib/build/recipe/oauth2provider/api/token.js new file mode 100644 index 000000000..9aaa0bd38 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/token.js @@ -0,0 +1,40 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function tokenPOST(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.tokenPOST === undefined) { + return false; + } + const authorizationHeader = options.req.getHeaderValue("authorization"); + let response = await apiImplementation.tokenPOST({ + authorizationHeader, + options, + body: await options.req.getBodyAsJSONOrFormData(), + userContext, + }); + if ("error" in response) { + utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = tokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/userInfo.d.ts b/lib/build/recipe/oauth2provider/api/userInfo.d.ts new file mode 100644 index 000000000..d0b8cdf4e --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/userInfo.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function userInfoGET( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/userInfo.js b/lib/build/recipe/oauth2provider/api/userInfo.js new file mode 100644 index 000000000..cfa8f704b --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/userInfo.js @@ -0,0 +1,84 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../recipe")); +const utils_1 = require("../../../utils"); +const __1 = require("../../.."); +async function userInfoGET(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.userInfoGET === undefined) { + return false; + } + const authHeader = options.req.getHeaderValue("authorization"); + if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { + utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + return true; + } + const accessToken = authHeader.replace(/^Bearer /, "").trim(); + let accessTokenPayload; + try { + const { + payload, + } = await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + userContext, + }); + accessTokenPayload = payload; + } catch (error) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 401); + return true; + } + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + !Array.isArray(accessTokenPayload.scp) + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 401); + return true; + } + const userId = accessTokenPayload.sub; + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage( + options.res, + "Couldn't find any user associated with the access token", + 401 + ); + return true; + } + const response = await apiImplementation.userInfoGET({ + accessTokenPayload, + user, + tenantId, + scopes: accessTokenPayload.scp, + options, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = userInfoGET; diff --git a/lib/build/recipe/oauth2provider/api/utils.d.ts b/lib/build/recipe/oauth2provider/api/utils.d.ts new file mode 100644 index 000000000..1bf243b1a --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/utils.d.ts @@ -0,0 +1,75 @@ +// @ts-nocheck +import { UserContext } from "../../../types"; +import { SessionContainerInterface } from "../../session/types"; +import { ErrorOAuth2, RecipeInterface } from "../types"; +export declare function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + cookies, + isDirectCall, + userContext, +}: { + recipeImplementation: RecipeInterface; + loginChallenge: string; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookies?: string; + userContext: UserContext; + isDirectCall: boolean; +}): Promise< + | ErrorOAuth2 + | { + status: string; + redirectTo: string; + cookies: string | undefined; + } + | { + redirectTo: string; + cookies: string | undefined; + status?: undefined; + } +>; +export declare function handleLoginInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie, + userContext, +}: { + response: { + redirectTo: string; + cookies?: string; + }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookie?: string; + userContext: UserContext; +}): Promise< + | { + redirectTo: string; + cookies?: string; + } + | ErrorOAuth2 +>; +export declare function handleLogoutInternalRedirects({ + response, + recipeImplementation, + session, + userContext, +}: { + response: { + redirectTo: string; + }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + userContext: UserContext; +}): Promise< + | { + redirectTo: string; + } + | ErrorOAuth2 +>; diff --git a/lib/build/recipe/oauth2provider/api/utils.js b/lib/build/recipe/oauth2provider/api/utils.js new file mode 100644 index 000000000..64d5fe32d --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/utils.js @@ -0,0 +1,285 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handleLogoutInternalRedirects = exports.handleLoginInternalRedirects = exports.loginGET = void 0; +const supertokens_1 = __importDefault(require("../../../supertokens")); +const constants_1 = require("../../multitenancy/constants"); +const session_1 = require("../../session"); +const constants_2 = require("../constants"); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +// API implementation for the loginGET function. +// Extracted for use in both apiImplementation and handleInternalRedirects. +async function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + cookies, + isDirectCall, + userContext, +}) { + var _a, _b; + const loginRequest = await recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + if (loginRequest.status === "ERROR") { + return loginRequest; + } + const sessionInfo = + session !== undefined + ? await session_1.getSessionInformation( + session === null || session === void 0 ? void 0 : session.getHandle() + ) + : undefined; + if (!sessionInfo) { + session = undefined; + } + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = + (_a = incomingAuthUrlQueryParams.get("prompt")) !== null && _a !== void 0 + ? _a + : incomingAuthUrlQueryParams.get("st_prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + if (maxAgeParam !== null) { + try { + const maxAgeParsed = Number.parseInt(maxAgeParam); + if (Number.isNaN(maxAgeParsed)) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + if (maxAgeParsed < 0) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age cannot be negative", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + } catch (_c) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + } + const tenantIdParam = incomingAuthUrlQueryParams.get("tenant_id"); + if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (["", null].includes(tenantIdParam) || session.getTenantId() === tenantIdParam) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo.timeCreated) + ) { + const accept = await recipeImplementation.acceptLoginRequest({ + challenge: loginChallenge, + subject: session.getUserId(), + identityProviderSessionId: session.getHandle(), + userContext, + }); + return { status: "REDIRECT", redirectTo: accept.redirectTo, cookies: cookies }; + } + if (shouldTryRefresh && promptParam !== "login") { + return { + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "try-refresh", + loginChallenge, + userContext, + }), + cookies: cookies, + }; + } + if (promptParam === "none") { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "login_required", + errorDescription: + "The Authorization Server requires End-User authentication. Prompt 'none' was requested, but no existing or expired login session was found.", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + return { + status: "REDIRECT", + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "login", + loginChallenge, + forceFreshAuth: session !== undefined || promptParam === "login", + tenantId: + tenantIdParam !== null && tenantIdParam !== void 0 ? tenantIdParam : constants_1.DEFAULT_TENANT_ID, + hint: (_b = loginRequest.oidcContext) === null || _b === void 0 ? void 0 : _b.login_hint, + userContext, + }), + cookies, + }; +} +exports.loginGET = loginGET; +function getMergedCookies({ origCookies = "", newCookies }) { + if (!newCookies) { + return origCookies; + } + const cookieMap = origCookies.split(";").reduce((acc, curr) => { + const [name, value] = curr.split("="); + return Object.assign(Object.assign({}, acc), { [name.trim()]: value }); + }, {}); + const setCookies = set_cookie_parser_1.default.parse(set_cookie_parser_1.default.splitCookiesString(newCookies)); + for (const { name, value, expires } of setCookies) { + if (expires && new Date(expires) < new Date()) { + delete cookieMap[name]; + } else { + cookieMap[name] = value; + } + } + return Object.entries(cookieMap) + .map(([key, value]) => `${key}=${value}`) + .join(";"); +} +function mergeSetCookieHeaders(setCookie1, setCookie2) { + if (!setCookie1) { + return setCookie2 || ""; + } + if (!setCookie2 || setCookie1 === setCookie2) { + return setCookie1; + } + return `${setCookie1}, ${setCookie2}`; +} +function isLoginInternalRedirect(redirectTo) { + const { apiDomain, apiBasePath } = supertokens_1.default.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + return [constants_2.LOGIN_PATH, constants_2.AUTH_PATH].some((path) => redirectTo.startsWith(`${basePath}${path}`)); +} +function isLogoutInternalRedirect(redirectTo) { + const { apiDomain, apiBasePath } = supertokens_1.default.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + return redirectTo.startsWith(`${basePath}${constants_2.END_SESSION_PATH}`); +} +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/login and /oauth/auth endpoints in the login flow. +async function handleLoginInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie = "", + userContext, +}) { + var _a; + if (!isLoginInternalRedirect(response.redirectTo)) { + return response; + } + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + while (redirectCount < maxRedirects && isLoginInternalRedirect(response.redirectTo)) { + cookie = getMergedCookies({ origCookies: cookie, newCookies: response.cookies }); + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + if (response.redirectTo.includes(constants_2.LOGIN_PATH)) { + const loginChallenge = + (_a = params.get("login_challenge")) !== null && _a !== void 0 ? _a : params.get("loginChallenge"); + if (!loginChallenge) { + throw new Error(`Expected loginChallenge in ${response.redirectTo}`); + } + const loginRes = await loginGET({ + recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + cookies: response.cookies, + isDirectCall: false, + userContext, + }); + if ("error" in loginRes) { + return loginRes; + } + response = { + redirectTo: loginRes.redirectTo, + cookies: mergeSetCookieHeaders(loginRes.cookies, response.cookies), + }; + } else if (response.redirectTo.includes(constants_2.AUTH_PATH)) { + const authRes = await recipeImplementation.authorization({ + params: Object.fromEntries(params.entries()), + cookies: cookie, + session, + userContext, + }); + if ("error" in authRes) { + return authRes; + } + response = { + redirectTo: authRes.redirectTo, + cookies: mergeSetCookieHeaders(authRes.cookies, response.cookies), + }; + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + redirectCount++; + } + return response; +} +exports.handleLoginInternalRedirects = handleLoginInternalRedirects; +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/end_session endpoint in the logout flow. +async function handleLogoutInternalRedirects({ response, recipeImplementation, session, userContext }) { + if (!isLogoutInternalRedirect(response.redirectTo)) { + return response; + } + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + while (redirectCount < maxRedirects && isLogoutInternalRedirect(response.redirectTo)) { + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + if (response.redirectTo.includes(constants_2.END_SESSION_PATH)) { + const endSessionRes = await recipeImplementation.endSession({ + params: Object.fromEntries(params.entries()), + session, + // We internally redirect to the `end_session_endpoint` at the end of the logout flow. + // This involves calling Hydra with the `logout_verifier`, after which Hydra redirects to the `post_logout_redirect_uri`. + // We set `shouldTryRefresh` to `false` since the SuperTokens session isn't needed to handle this request. + shouldTryRefresh: false, + userContext, + }); + if ("error" in endSessionRes) { + return endSessionRes; + } + response = endSessionRes; + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + redirectCount++; + } + return response; +} +exports.handleLogoutInternalRedirects = handleLogoutInternalRedirects; diff --git a/lib/build/recipe/oauth2provider/constants.d.ts b/lib/build/recipe/oauth2provider/constants.d.ts new file mode 100644 index 000000000..56069cece --- /dev/null +++ b/lib/build/recipe/oauth2provider/constants.d.ts @@ -0,0 +1,11 @@ +// @ts-nocheck +export declare const OAUTH2_BASE_PATH = "/oauth/"; +export declare const LOGIN_PATH = "/oauth/login"; +export declare const AUTH_PATH = "/oauth/auth"; +export declare const TOKEN_PATH = "/oauth/token"; +export declare const LOGIN_INFO_PATH = "/oauth/login/info"; +export declare const USER_INFO_PATH = "/oauth/userinfo"; +export declare const REVOKE_TOKEN_PATH = "/oauth/revoke"; +export declare const INTROSPECT_TOKEN_PATH = "/oauth/introspect"; +export declare const END_SESSION_PATH = "/oauth/end_session"; +export declare const LOGOUT_PATH = "/oauth/logout"; diff --git a/lib/build/recipe/oauth2provider/constants.js b/lib/build/recipe/oauth2provider/constants.js new file mode 100644 index 000000000..13cc9ad7f --- /dev/null +++ b/lib/build/recipe/oauth2provider/constants.js @@ -0,0 +1,27 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LOGOUT_PATH = exports.END_SESSION_PATH = exports.INTROSPECT_TOKEN_PATH = exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0; +exports.OAUTH2_BASE_PATH = "/oauth/"; +exports.LOGIN_PATH = "/oauth/login"; +exports.AUTH_PATH = "/oauth/auth"; +exports.TOKEN_PATH = "/oauth/token"; +exports.LOGIN_INFO_PATH = "/oauth/login/info"; +exports.USER_INFO_PATH = "/oauth/userinfo"; +exports.REVOKE_TOKEN_PATH = "/oauth/revoke"; +exports.INTROSPECT_TOKEN_PATH = "/oauth/introspect"; +exports.END_SESSION_PATH = "/oauth/end_session"; +exports.LOGOUT_PATH = "/oauth/logout"; diff --git a/lib/build/recipe/oauth2provider/index.d.ts b/lib/build/recipe/oauth2provider/index.d.ts new file mode 100644 index 000000000..f859cac0d --- /dev/null +++ b/lib/build/recipe/oauth2provider/index.d.ts @@ -0,0 +1,145 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import { + APIInterface, + RecipeInterface, + APIOptions, + CreateOAuth2ClientInput, + UpdateOAuth2ClientInput, + DeleteOAuth2ClientInput, + GetOAuth2ClientsInput, +} from "./types"; +export default class Wrapper { + static init: typeof Recipe.init; + static getOAuth2Client( + clientId: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + static getOAuth2Clients( + input: GetOAuth2ClientsInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + clients: import("./OAuth2Client").OAuth2Client[]; + nextPaginationToken?: string | undefined; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + static createOAuth2Client( + input: CreateOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + static updateOAuth2Client( + input: UpdateOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + static deleteOAuth2Client( + input: DeleteOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + static validateOAuth2AccessToken( + token: string, + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }, + checkDatabase?: boolean, + userContext?: Record + ): Promise<{ + status: "OK"; + payload: import("../usermetadata").JSONObject; + }>; + static createTokenForClientCredentials( + clientId: string, + clientSecret: string, + scope?: string[], + audience?: string, + userContext?: Record + ): Promise; + static revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ): Promise< + | import("./types").ErrorOAuth2 + | { + status: "OK"; + } + >; + static revokeTokensByClientId( + clientId: string, + userContext?: Record + ): Promise<{ + status: "OK"; + }>; + static revokeTokensBySessionHandle( + sessionHandle: string, + userContext?: Record + ): Promise<{ + status: "OK"; + }>; + static validateOAuth2RefreshToken( + token: string, + scopes?: string[], + userContext?: Record + ): Promise; +} +export declare let init: typeof Recipe.init; +export declare let getOAuth2Client: typeof Wrapper.getOAuth2Client; +export declare let getOAuth2Clients: typeof Wrapper.getOAuth2Clients; +export declare let createOAuth2Client: typeof Wrapper.createOAuth2Client; +export declare let updateOAuth2Client: typeof Wrapper.updateOAuth2Client; +export declare let deleteOAuth2Client: typeof Wrapper.deleteOAuth2Client; +export declare let validateOAuth2AccessToken: typeof Wrapper.validateOAuth2AccessToken; +export declare let validateOAuth2RefreshToken: typeof Wrapper.validateOAuth2RefreshToken; +export declare let createTokenForClientCredentials: typeof Wrapper.createTokenForClientCredentials; +export declare let revokeToken: typeof Wrapper.revokeToken; +export declare let revokeTokensByClientId: typeof Wrapper.revokeTokensByClientId; +export declare let revokeTokensBySessionHandle: typeof Wrapper.revokeTokensBySessionHandle; +export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/build/recipe/oauth2provider/index.js b/lib/build/recipe/oauth2provider/index.js new file mode 100644 index 000000000..ee4d9b5ed --- /dev/null +++ b/lib/build/recipe/oauth2provider/index.js @@ -0,0 +1,141 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.revokeTokensBySessionHandle = exports.revokeTokensByClientId = exports.revokeToken = exports.createTokenForClientCredentials = exports.validateOAuth2RefreshToken = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.getOAuth2Client = exports.init = void 0; +const utils_1 = require("../../utils"); +const recipe_1 = __importDefault(require("./recipe")); +class Wrapper { + static async getOAuth2Client(clientId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client({ + clientId, + userContext: utils_1.getUserContext(userContext), + }); + } + static async getOAuth2Clients(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.getOAuth2Clients( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async createOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async updateOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.updateOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async deleteOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.deleteOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static validateOAuth2AccessToken(token, requirements, checkDatabase, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token, + requirements, + checkDatabase, + userContext: utils_1.getUserContext(userContext), + }); + } + static createTokenForClientCredentials(clientId, clientSecret, scope, audience, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.tokenExchange({ + body: { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + scope: scope === null || scope === void 0 ? void 0 : scope.join(" "), + audience: audience, + }, + userContext: utils_1.getUserContext(userContext), + }); + } + static async revokeToken(token, clientId, clientSecret, userContext) { + let authorizationHeader = undefined; + const normalisedUserContext = utils_1.getUserContext(userContext); + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId, userContext: normalisedUserContext }); + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + const { tokenEndpointAuthMethod } = res.client; + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } + static async revokeTokensByClientId(clientId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeTokensByClientId({ + clientId, + userContext: utils_1.getUserContext(userContext), + }); + } + static async revokeTokensBySessionHandle(sessionHandle, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeTokensBySessionHandle({ + sessionHandle, + userContext: utils_1.getUserContext(userContext), + }); + } + static validateOAuth2RefreshToken(token, scopes, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.introspectToken({ + token, + scopes, + userContext: utils_1.getUserContext(userContext), + }); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +exports.init = Wrapper.init; +exports.getOAuth2Client = Wrapper.getOAuth2Client; +exports.getOAuth2Clients = Wrapper.getOAuth2Clients; +exports.createOAuth2Client = Wrapper.createOAuth2Client; +exports.updateOAuth2Client = Wrapper.updateOAuth2Client; +exports.deleteOAuth2Client = Wrapper.deleteOAuth2Client; +exports.validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; +exports.validateOAuth2RefreshToken = Wrapper.validateOAuth2RefreshToken; +exports.createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; +exports.revokeToken = Wrapper.revokeToken; +exports.revokeTokensByClientId = Wrapper.revokeTokensByClientId; +exports.revokeTokensBySessionHandle = Wrapper.revokeTokensBySessionHandle; diff --git a/lib/build/recipe/oauth2provider/recipe.d.ts b/lib/build/recipe/oauth2provider/recipe.d.ts new file mode 100644 index 000000000..d2739dbf7 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipe.d.ts @@ -0,0 +1,67 @@ +// @ts-nocheck +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import NormalisedURLPath from "../../normalisedURLPath"; +import RecipeModule from "../../recipeModule"; +import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; +import { + APIInterface, + PayloadBuilderFunction, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + UserInfo, + UserInfoBuilderFunction, +} from "./types"; +import { User } from "../../user"; +export default class Recipe extends RecipeModule { + static RECIPE_ID: string; + private static instance; + private accessTokenBuilders; + private idTokenBuilders; + private userInfoBuilders; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); + static getInstance(): Recipe | undefined; + static getInstanceOrThrowError(): Recipe; + static init(config?: TypeInput): RecipeListFunction; + static reset(): void; + addUserInfoBuilderFromOtherRecipe: (userInfoBuilderFn: UserInfoBuilderFunction) => void; + addAccessTokenBuilderFromOtherRecipe: (accessTokenBuilders: PayloadBuilderFunction) => void; + addIdTokenBuilderFromOtherRecipe: (idTokenBuilder: PayloadBuilderFunction) => void; + getAPIsHandled(): APIHandled[]; + handleAPIRequest: ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + method: HTTPMethod, + userContext: UserContext + ) => Promise; + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise; + getAllCORSHeaders(): string[]; + isErrorFromThisRecipe(err: any): err is error; + getDefaultAccessTokenPayload( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ): Promise; + getDefaultIdTokenPayload( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ): Promise; + getDefaultUserInfoPayload( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext + ): Promise; +} diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js new file mode 100644 index 000000000..4c31ae8cc --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -0,0 +1,280 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const error_1 = __importDefault(require("../../error")); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const querier_1 = require("../../querier"); +const recipeModule_1 = __importDefault(require("../../recipeModule")); +const auth_1 = __importDefault(require("./api/auth")); +const implementation_1 = __importDefault(require("./api/implementation")); +const login_1 = __importDefault(require("./api/login")); +const token_1 = __importDefault(require("./api/token")); +const loginInfo_1 = __importDefault(require("./api/loginInfo")); +const constants_1 = require("./constants"); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const utils_1 = require("./utils"); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +const userInfo_1 = __importDefault(require("./api/userInfo")); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const revokeToken_1 = __importDefault(require("./api/revokeToken")); +const introspectToken_1 = __importDefault(require("./api/introspectToken")); +const endSession_1 = require("./api/endSession"); +const logout_1 = require("./api/logout"); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, isInServerlessEnv, config) { + super(recipeId, appInfo); + this.accessTokenBuilders = []; + this.idTokenBuilders = []; + this.userInfoBuilders = []; + this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => { + this.userInfoBuilders.push(userInfoBuilderFn); + }; + this.addAccessTokenBuilderFromOtherRecipe = (accessTokenBuilders) => { + this.accessTokenBuilders.push(accessTokenBuilders); + }; + this.addIdTokenBuilderFromOtherRecipe = (idTokenBuilder) => { + this.idTokenBuilders.push(idTokenBuilder); + }; + this.handleAPIRequest = async (id, tenantId, req, res, _path, method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + if (id === constants_1.LOGIN_PATH) { + return login_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.TOKEN_PATH) { + return token_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.AUTH_PATH) { + return auth_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.LOGIN_INFO_PATH) { + return loginInfo_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.USER_INFO_PATH) { + return userInfo_1.default(this.apiImpl, tenantId, options, userContext); + } + if (id === constants_1.REVOKE_TOKEN_PATH) { + return revokeToken_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.INTROSPECT_TOKEN_PATH) { + return introspectToken_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.END_SESSION_PATH && method === "get") { + return endSession_1.endSessionGET(this.apiImpl, options, userContext); + } + if (id === constants_1.END_SESSION_PATH && method === "post") { + return endSession_1.endSessionPOST(this.apiImpl, options, userContext); + } + if (id === constants_1.LOGOUT_PATH && method === "post") { + return logout_1.logoutPOST(this.apiImpl, options, userContext); + } + throw new Error("Should never come here: handleAPIRequest called with unknown id"); + }; + this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + { + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default( + querier_1.Querier.getNewInstanceOrThrowError(recipeId), + this.config, + appInfo, + this.getDefaultAccessTokenPayload.bind(this), + this.getDefaultIdTokenPayload.bind(this), + this.getDefaultUserInfoPayload.bind(this) + ) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new supertokens_js_override_1.default(implementation_1.default()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + /* Init functions */ + static getInstance() { + return Recipe.instance; + } + static getInstanceOrThrowError() { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the Jwt.init function?"); + } + static init(config) { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + return Recipe.instance; + } else { + throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs."); + } + }; + } + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + combinedRemoteJWKSet_1.resetCombinedJWKS(); + Recipe.instance = undefined; + } + /* RecipeModule functions */ + getAPIsHandled() { + return [ + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.LOGIN_PATH), + id: constants_1.LOGIN_PATH, + disabled: this.apiImpl.loginGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.TOKEN_PATH), + id: constants_1.TOKEN_PATH, + disabled: this.apiImpl.tokenPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.AUTH_PATH), + id: constants_1.AUTH_PATH, + disabled: this.apiImpl.authGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.LOGIN_INFO_PATH), + id: constants_1.LOGIN_INFO_PATH, + disabled: this.apiImpl.loginInfoGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.USER_INFO_PATH), + id: constants_1.USER_INFO_PATH, + disabled: this.apiImpl.userInfoGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REVOKE_TOKEN_PATH), + id: constants_1.REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.INTROSPECT_TOKEN_PATH), + id: constants_1.INTROSPECT_TOKEN_PATH, + disabled: this.apiImpl.introspectTokenPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.END_SESSION_PATH), + id: constants_1.END_SESSION_PATH, + disabled: this.apiImpl.endSessionGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.END_SESSION_PATH), + id: constants_1.END_SESSION_PATH, + disabled: this.apiImpl.endSessionPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.LOGOUT_PATH), + id: constants_1.LOGOUT_PATH, + disabled: this.apiImpl.logoutPOST === undefined, + }, + ]; + } + handleError(error, _, __, _userContext) { + throw error; + } + getAllCORSHeaders() { + return []; + } + isErrorFromThisRecipe(err) { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + async getDefaultAccessTokenPayload(user, scopes, sessionHandle, userContext) { + let payload = {}; + for (const fn of this.accessTokenBuilders) { + payload = Object.assign(Object.assign({}, payload), await fn(user, scopes, sessionHandle, userContext)); + } + return payload; + } + async getDefaultIdTokenPayload(user, scopes, sessionHandle, userContext) { + let payload = {}; + if (scopes.includes("email")) { + payload.email = user === null || user === void 0 ? void 0 : user.emails[0]; + payload.email_verified = user.loginMethods.some( + (lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified + ); + payload.emails = user.emails; + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => + lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && + lm.verified + ); + payload.phoneNumbers = user.phoneNumbers; + } + for (const fn of this.idTokenBuilders) { + payload = Object.assign(Object.assign({}, payload), await fn(user, scopes, sessionHandle, userContext)); + } + return payload; + } + async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext) { + let payload = { + sub: accessTokenPayload.sub, + }; + if (scopes.includes("email")) { + // TODO: try and get the email based on the user id of the entire user object + payload.email = user === null || user === void 0 ? void 0 : user.emails[0]; + payload.email_verified = user.loginMethods.some( + (lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified + ); + payload.emails = user.emails; + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => + lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && + lm.verified + ); + payload.phoneNumbers = user.phoneNumbers; + } + for (const fn of this.userInfoBuilders) { + payload = Object.assign( + Object.assign({}, payload), + await fn(user, accessTokenPayload, scopes, tenantId, userContext) + ); + } + return payload; + } +} +exports.default = Recipe; +Recipe.RECIPE_ID = "oauth2provider"; +Recipe.instance = undefined; diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.d.ts b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts new file mode 100644 index 000000000..4ecaeef69 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +import { Querier } from "../../querier"; +import { NormalisedAppinfo } from "../../types"; +import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction, UserInfoBuilderFunction } from "./types"; +export default function getRecipeInterface( + querier: Querier, + _config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, + getDefaultAccessTokenPayload: PayloadBuilderFunction, + getDefaultIdTokenPayload: PayloadBuilderFunction, + getDefaultUserInfoPayload: UserInfoBuilderFunction +): RecipeInterface; diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js new file mode 100644 index 000000000..cde274874 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -0,0 +1,845 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const jose = __importStar(require("jose")); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const OAuth2Client_1 = require("./OAuth2Client"); +const __1 = require("../.."); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const recipe_1 = __importDefault(require("../session/recipe")); +const recipe_2 = __importDefault(require("../openid/recipe")); +const constants_1 = require("../multitenancy/constants"); +function getUpdatedRedirectTo(appInfo, redirectTo) { + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); +} +function copyAndCleanRequestBodyInput(input) { + let result = Object.assign({}, input); + delete result.userContext; + delete result.tenantId; + delete result.session; + return result; +} +function getRecipeInterface( + querier, + _config, + appInfo, + getDefaultAccessTokenPayload, + getDefaultIdTokenPayload, + getDefaultUserInfoPayload +) { + return { + getLoginRequest: async function (input) { + const resp = await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/login"), + { loginChallenge: input.challenge }, + input.userContext + ); + if (resp.status !== "OK") { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + return { + status: "OK", + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptLoginRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/accept`), + { + acr: input.acr, + amr: input.amr, + context: input.context, + extendSessionLifespan: input.extendSessionLifespan, + identityProviderSessionId: input.identityProviderSessionId, + subject: input.subject, + }, + { + loginChallenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectLoginRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/reject`), + { + error: input.error.error, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, + }, + { + login_challenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + getConsentRequest: async function (input) { + const resp = await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/consent"), + { consentChallenge: input.challenge }, + input.userContext + ); + return { + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptConsentRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/accept`), + { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + iss: await recipe_2.default.getIssuer(input.userContext), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, + initialAccessTokenPayload: input.initialAccessTokenPayload, + initialIdTokenPayload: input.initialIdTokenPayload, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectConsentRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/reject`), + { + error: input.error.error, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + authorization: async function (input) { + var _a, _b, _c, _d, _e; + // we handle this in the backend SDK level + if (input.params.prompt === "none") { + input.params["st_prompt"] = "none"; + delete input.params.prompt; + } + let payloads; + if (input.params.client_id === undefined || typeof input.params.client_id !== "string") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required and must be a string", + }; + } + const scopes = await this.getRequestedScopes({ + scopeParam: ((_a = input.params.scope) === null || _a === void 0 ? void 0 : _a.split(" ")) || [], + clientId: input.params.client_id, + recipeUserId: (_b = input.session) === null || _b === void 0 ? void 0 : _b.getRecipeUserId(), + sessionHandle: (_c = input.session) === null || _c === void 0 ? void 0 : _c.getHandle(), + userContext: input.userContext, + }); + const responseTypes = + (_e = (_d = input.params.response_type) === null || _d === void 0 ? void 0 : _d.split(" ")) !== null && + _e !== void 0 + ? _e + : []; + if (input.session !== undefined) { + const clientInfo = await this.getOAuth2Client({ + clientId: input.params.client_id, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + const user = await __1.getUser(input.session.getUserId()); + if (!user) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "User deleted", + }; + } + // These default to an empty objects, because we want to keep them as a required input + // but they'll not be actually used in the flows where we are not building them. + const idToken = + scopes.includes("openid") && (responseTypes.includes("id_token") || responseTypes.includes("code")) + ? await this.buildIdTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes, + userContext: input.userContext, + }) + : {}; + const accessToken = + responseTypes.includes("token") || responseTypes.includes("code") + ? await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes, + userContext: input.userContext, + }) + : {}; + payloads = { + idToken, + accessToken, + }; + } + const resp = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth`), + { + params: Object.assign(Object.assign({}, input.params), { scope: scopes.join(" ") }), + cookies: input.cookies, + session: payloads, + }, + input.userContext + ); + if (resp.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "The provided client_id is not valid", + }; + } + if (resp.status !== "OK") { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + if (redirectTo === undefined) { + throw new Error(resp.body); + } + const redirectToURL = new URL(redirectTo); + const consentChallenge = redirectToURL.searchParams.get("consent_challenge"); + if (consentChallenge !== null && input.session !== undefined) { + const consentRequest = await this.getConsentRequest({ + challenge: consentChallenge, + userContext: input.userContext, + }); + const consentRes = await this.acceptConsentRequest({ + userContext: input.userContext, + challenge: consentRequest.challenge, + grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, + grantScope: consentRequest.requestedScope, + tenantId: input.session.getTenantId(), + rsub: input.session.getRecipeUserId().getAsString(), + sessionHandle: input.session.getHandle(), + initialAccessTokenPayload: payloads === null || payloads === void 0 ? void 0 : payloads.accessToken, + initialIdTokenPayload: payloads === null || payloads === void 0 ? void 0 : payloads.idToken, + }); + return { + redirectTo: consentRes.redirectTo, + cookies: resp.cookies, + }; + } + return { redirectTo, cookies: resp.cookies }; + }, + tokenExchange: async function (input) { + var _a, _b, _c, _d; + const body = { + inputBody: input.body, + authorizationHeader: input.authorizationHeader, + }; + body.iss = await recipe_2.default.getIssuer(input.userContext); + if (input.body.grant_type === "password") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "Unsupported grant type: password", + }; + } + if (input.body.grant_type === "client_credentials") { + if (input.body.client_id === undefined) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required", + }; + } + const scopes = + (_b = (_a = input.body.scope) === null || _a === void 0 ? void 0 : _a.split(" ")) !== null && + _b !== void 0 + ? _b + : []; + const clientInfo = await this.getOAuth2Client({ + clientId: input.body.client_id, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + body["id_token"] = await this.buildIdTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + body["access_token"] = await this.buildAccessTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + } + if (input.body.grant_type === "refresh_token") { + const scopes = + (_d = (_c = input.body.scope) === null || _c === void 0 ? void 0 : _c.split(" ")) !== null && + _d !== void 0 + ? _d + : []; + const tokenInfo = await this.introspectToken({ + token: input.body.refresh_token, + scopes, + userContext: input.userContext, + }); + if (tokenInfo.active === true) { + const sessionHandle = tokenInfo.sessionHandle; + const clientInfo = await this.getOAuth2Client({ + clientId: tokenInfo.client_id, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + const user = await __1.getUser(tokenInfo.sub); + if (!user) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "User not found", + }; + } + body["id_token"] = await this.buildIdTokenPayload({ + user, + client, + sessionHandle, + scopes, + userContext: input.userContext, + }); + body["access_token"] = await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + } + } + if (input.authorizationHeader) { + body["authorizationHeader"] = input.authorizationHeader; + } + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/token`), + body, + input.userContext + ); + if (res.status !== "OK") { + return { + status: "ERROR", + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + return res; + }, + getOAuth2Clients: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default(`/recipe/oauth/clients/list`), + { + pageSize: input.pageSize, + clientName: input.clientName, + pageToken: input.paginationToken, + }, + {}, + input.userContext + ); + if (response.body.status === "OK") { + return { + status: "OK", + clients: response.body.clients.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), + nextPaginationToken: response.body.nextPaginationToken, + }; + } else { + return { + status: "ERROR", + error: response.body.error, + errorDescription: response.body.errorDescription, + }; + } + }, + getOAuth2Client: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + { clientId: input.clientId }, + {}, + input.userContext + ); + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body), + }; + } else if (response.body.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + error: "invalid_request", + errorDescription: "The provided client_id is not valid or unknown", + }; + } else { + return { + status: "ERROR", + error: response.body.error, + errorDescription: response.body.errorDescription, + }; + } + }, + createOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + input.userContext + ); + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response), + }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + updateOAuth2Client: async function (input) { + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + { clientId: input.clientId }, + input.userContext + ); + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response), + }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + deleteOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/clients/remove`), + { clientId: input.clientId }, + input.userContext + ); + if (response.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + getRequestedScopes: async function (input) { + return input.scopeParam; + }, + buildAccessTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } + return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildIdTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } + return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); + }, + getFrontendRedirectionURL: async function (input) { + const websiteDomain = appInfo + .getOrigin({ request: undefined, userContext: input.userContext }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.tenantId !== undefined && input.tenantId !== constants_1.DEFAULT_TENANT_ID) { + queryParams.set("tenantId", input.tenantId); + } + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + return `${websiteDomain}${websiteBasePath}?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `${websiteDomain}${websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `${websiteDomain}${websiteBasePath}`; + } else if (input.type === "logout-confirmation") { + return `${websiteDomain}${websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`; + } + throw new Error("This should never happen: invalid type passed to getFrontendRedirectionURL"); + }, + validateOAuth2AccessToken: async function (input) { + var _a, _b, _c; + const payload = ( + await jose.jwtVerify( + input.token, + combinedRemoteJWKSet_1.getCombinedJWKS(recipe_1.default.getInstanceOrThrowError().config) + ) + ).payload; + if (payload.stt !== 1) { + throw new Error("Wrong token type"); + } + if ( + ((_a = input.requirements) === null || _a === void 0 ? void 0 : _a.clientId) !== undefined && + payload.client_id !== input.requirements.clientId + ) { + throw new Error( + `The token doesn't belong to the specified client (${input.requirements.clientId} !== ${payload.client_id})` + ); + } + if ( + ((_b = input.requirements) === null || _b === void 0 ? void 0 : _b.scopes) !== undefined && + input.requirements.scopes.some((scope) => !payload.scp.includes(scope)) + ) { + throw new Error("The token is missing some required scopes"); + } + const aud = payload.aud instanceof Array ? payload.aud : [payload.aud]; + if ( + ((_c = input.requirements) === null || _c === void 0 ? void 0 : _c.audience) !== undefined && + !aud.includes(input.requirements.audience) + ) { + throw new Error("The token doesn't belong to the specified audience"); + } + if (input.checkDatabase) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/introspect`), + { + token: input.token, + }, + input.userContext + ); + if (response.active !== true) { + throw new Error("The token is expired, invalid or has been revoked"); + } + } + return { status: "OK", payload: payload }; + }, + revokeToken: async function (input) { + const requestBody = { + token: input.token, + }; + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input && input.clientId !== undefined) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input && input.clientSecret !== undefined) { + requestBody.client_secret = input.clientSecret; + } + } + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/token/revoke`), + requestBody, + input.userContext + ); + if (res.status !== "OK") { + return { + status: "ERROR", + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + return { status: "OK" }; + }, + revokeTokensBySessionHandle: async function (input) { + await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/session/revoke`), + { sessionHandle: input.sessionHandle }, + input.userContext + ); + return { status: "OK" }; + }, + revokeTokensByClientId: async function (input) { + await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/tokens/revoke`), + { clientId: input.clientId }, + input.userContext + ); + return { status: "OK" }; + }, + introspectToken: async function ({ token, scopes, userContext }) { + // Determine if the token is an access token by checking if it doesn't start with "st_rt" + const isAccessToken = !token.startsWith("st_rt"); + // Attempt to validate the access token locally + // If it fails, the token is not active, and we return early + if (isAccessToken) { + try { + await this.validateOAuth2AccessToken({ + token, + requirements: { scopes }, + checkDatabase: false, + userContext, + }); + } catch (error) { + return { active: false }; + } + } + // For tokens that passed local validation or if it's a refresh token, + // validate the token with the database by calling the core introspection endpoint + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/introspect`), + { + token, + scope: scopes ? scopes.join(" ") : undefined, + }, + userContext + ); + return res; + }, + endSession: async function (input) { + /** + * NOTE: The API response has 3 possible cases: + * + * CASE 1: `end_session` request with a valid `id_token_hint` + * - Redirects to `/oauth/logout` with a `logout_challenge`. + * + * CASE 2: `end_session` request with an already logged out `id_token_hint` + * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. + * + * CASE 3: `end_session` request with a `logout_verifier` (after accepting the logout request) + * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. + */ + const resp = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/recipe/oauth/sessions/logout`), + { + clientId: input.params.client_id, + idTokenHint: input.params.id_token_hint, + postLogoutRedirectUri: input.params.post_logout_redirect_uri, + state: input.params.state, + logoutVerifier: input.params.logout_verifier, + }, + input.userContext + ); + if ("error" in resp) { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + let redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + const initialRedirectToURL = new URL(redirectTo); + const logoutChallenge = initialRedirectToURL.searchParams.get("logout_challenge"); + // CASE 1 (See above notes) + if (logoutChallenge !== null) { + // Redirect to the frontend to ask for logout confirmation if there is a valid or expired supertokens session + if (input.session !== undefined || input.shouldTryRefresh) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "logout-confirmation", + logoutChallenge, + userContext: input.userContext, + }), + }; + } else { + // Accept the logout challenge immediately as there is no supertokens session + redirectTo = ( + await this.acceptLogoutRequest({ + challenge: logoutChallenge, + userContext: input.userContext, + }) + ).redirectTo; + } + } + // CASE 2 or 3 (See above notes) + // NOTE: If no post_logout_redirect_uri is provided, Hydra redirects to a fallback page. + // In this case, we redirect the user to the /auth page. + if (redirectTo.endsWith("/fallbacks/logout/callback")) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; + } + return { redirectTo }; + }, + acceptLogoutRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/logout/accept`), + { challenge: input.challenge }, + {}, + input.userContext + ); + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + if (redirectTo.endsWith("/fallbacks/logout/callback")) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; + } + return { redirectTo }; + }, + rejectLogoutRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/logout/reject`), + {}, + { challenge: input.challenge }, + input.userContext + ); + if (resp.status != "OK") { + throw new Error(resp.error); + } + return { status: "OK" }; + }, + }; +} +exports.default = getRecipeInterface; diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts new file mode 100644 index 000000000..7d3987138 --- /dev/null +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -0,0 +1,578 @@ +// @ts-nocheck +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; +import { SessionContainerInterface } from "../session/types"; +import { OAuth2Client } from "./OAuth2Client"; +import { User } from "../../user"; +import RecipeUserId from "../../recipeUserId"; +export declare type TypeInput = { + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeNormalisedInput = { + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; +}; +export declare type ErrorOAuth2 = { + status: "ERROR"; + error: string; + errorDescription: string; + statusCode?: number; +}; +export declare type ConsentRequest = { + acr?: string; + amr?: string[]; + challenge: string; + client?: OAuth2Client; + context?: JSONObject; + loginChallenge?: string; + loginSessionId?: string; + oidcContext?: any; + requestedAccessTokenAudience?: string[]; + requestedScope?: string[]; + skip?: boolean; + subject?: string; +}; +export declare type LoginRequest = { + challenge: string; + client: OAuth2Client; + oidcContext?: any; + requestUrl: string; + requestedAccessTokenAudience?: string[]; + requestedScope?: string[]; + sessionId?: string; + skip: boolean; + subject: string; +}; +export declare type TokenInfo = { + access_token?: string; + expires_in: number; + id_token?: string; + refresh_token?: string; + scope: string; + token_type: string; +}; +export declare type LoginInfo = { + clientId: string; + clientName: string; + tosUri?: string; + policyUri?: string; + logoUri?: string; + clientUri?: string; + metadata?: Record | null; +}; +export declare type UserInfo = { + sub: string; + email?: string; + email_verified?: boolean; + phoneNumber?: string; + phoneNumber_verified?: boolean; + [key: string]: JSONValue; +}; +export declare type InstrospectTokenResponse = + | { + active: false; + } + | ({ + active: true; + } & JSONObject); +export declare type RecipeInterface = { + authorization(input: { + params: Record; + cookies: string | undefined; + session: SessionContainerInterface | undefined; + userContext: UserContext; + }): Promise< + | { + redirectTo: string; + cookies: string | undefined; + } + | ErrorOAuth2 + >; + tokenExchange(input: { + authorizationHeader?: string; + body: Record; + userContext: UserContext; + }): Promise; + getConsentRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptConsentRequest(input: { + challenge: string; + context?: any; + grantAccessTokenAudience?: string[]; + grantScope?: string[]; + handledAt?: string; + tenantId: string; + rsub: string; + sessionHandle: string; + initialAccessTokenPayload: JSONObject | undefined; + initialIdTokenPayload: JSONObject | undefined; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + rejectConsentRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + getLoginRequest(input: { + challenge: string; + userContext: UserContext; + }): Promise< + | (LoginRequest & { + status: "OK"; + }) + | ErrorOAuth2 + >; + acceptLoginRequest(input: { + challenge: string; + acr?: string; + amr?: string[]; + context?: any; + extendSessionLifespan?: boolean; + identityProviderSessionId?: string; + subject: string; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + rejectLoginRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + getOAuth2Client(input: { + clientId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + getOAuth2Clients( + input: GetOAuth2ClientsInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + clients: Array; + nextPaginationToken?: string; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + createOAuth2Client( + input: CreateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + updateOAuth2Client( + input: UpdateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + deleteOAuth2Client( + input: DeleteOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + validateOAuth2AccessToken(input: { + token: string; + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }; + checkDatabase?: boolean; + userContext: UserContext; + }): Promise<{ + status: "OK"; + payload: JSONObject; + }>; + getRequestedScopes(input: { + recipeUserId: RecipeUserId | undefined; + sessionHandle: string | undefined; + scopeParam: string[]; + clientId: string; + userContext: UserContext; + }): Promise; + buildAccessTokenPayload(input: { + user: User | undefined; + client: OAuth2Client; + sessionHandle: string | undefined; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildIdTokenPayload(input: { + user: User | undefined; + client: OAuth2Client; + sessionHandle: string | undefined; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildUserInfo(input: { + user: User; + accessTokenPayload: JSONObject; + scopes: string[]; + tenantId: string; + userContext: UserContext; + }): Promise; + getFrontendRedirectionURL( + input: + | { + type: "login"; + loginChallenge: string; + tenantId: string; + forceFreshAuth: boolean; + hint: string | undefined; + userContext: UserContext; + } + | { + type: "try-refresh"; + loginChallenge: string; + userContext: UserContext; + } + | { + type: "logout-confirmation"; + logoutChallenge: string; + userContext: UserContext; + } + | { + type: "post-logout-fallback"; + userContext: UserContext; + } + ): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ): Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >; + revokeTokensByClientId(input: { + clientId: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + }>; + revokeTokensBySessionHandle(input: { + sessionHandle: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + }>; + introspectToken(input: { + token: string; + scopes?: string[]; + userContext: UserContext; + }): Promise; + endSession(input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }): Promise< + | { + redirectTo: string; + } + | ErrorOAuth2 + >; + acceptLogoutRequest(input: { + challenge: string; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + rejectLogoutRequest(input: { + challenge: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + }>; +}; +export declare type APIInterface = { + loginGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }) => Promise< + | { + frontendRedirectTo: string; + cookies?: string; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + authGET: + | undefined + | ((input: { + params: any; + cookie: string | undefined; + session: SessionContainerInterface | undefined; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + redirectTo: string; + cookies?: string; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + tokenPOST: + | undefined + | ((input: { + authorizationHeader?: string; + body: any; + options: APIOptions; + userContext: UserContext; + }) => Promise); + loginInfoGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + info: LoginInfo; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + userInfoGET: + | undefined + | ((input: { + accessTokenPayload: JSONObject; + user: User; + scopes: string[]; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ) => Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >); + introspectTokenPOST: + | undefined + | ((input: { + token: string; + scopes?: string[]; + options: APIOptions; + userContext: UserContext; + }) => Promise); + endSessionGET: + | undefined + | ((input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + redirectTo: string; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + endSessionPOST: + | undefined + | ((input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + redirectTo: string; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + logoutPOST: + | undefined + | ((input: { + logoutChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + frontendRedirectTo: string; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); +}; +export declare type OAuth2ClientOptions = { + clientId: string; + clientSecret?: string; + createdAt: string; + updatedAt: string; + clientName: string; + scope: string; + redirectUris?: string[] | null; + postLogoutRedirectUris?: string[]; + authorizationCodeGrantAccessTokenLifespan?: string | null; + authorizationCodeGrantIdTokenLifespan?: string | null; + authorizationCodeGrantRefreshTokenLifespan?: string | null; + clientCredentialsGrantAccessTokenLifespan?: string | null; + implicitGrantAccessTokenLifespan?: string | null; + implicitGrantIdTokenLifespan?: string | null; + refreshTokenGrantAccessTokenLifespan?: string | null; + refreshTokenGrantIdTokenLifespan?: string | null; + refreshTokenGrantRefreshTokenLifespan?: string | null; + tokenEndpointAuthMethod: string; + audience?: string[]; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + clientUri?: string; + logoUri?: string; + policyUri?: string; + tosUri?: string; + metadata?: Record; +}; +export declare type GetOAuth2ClientsInput = { + /** + * Items per Page. Defaults to 250. + */ + pageSize?: number; + /** + * Next Page Token. Defaults to "1". + */ + paginationToken?: string; + /** + * The name of the clients to filter by. + */ + clientName?: string; +}; +export declare type CreateOAuth2ClientInput = Partial< + Omit +>; +export declare type UpdateOAuth2ClientInput = NonNullableProperties< + Omit +> & { + clientId: string; + redirectUris?: string[] | null; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + metadata?: Record | null; +}; +export declare type DeleteOAuth2ClientInput = { + clientId: string; +}; +export declare type PayloadBuilderFunction = ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext +) => Promise; +export declare type UserInfoBuilderFunction = ( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext +) => Promise; diff --git a/lib/build/recipe/oauth2provider/types.js b/lib/build/recipe/oauth2provider/types.js new file mode 100644 index 000000000..9f1237319 --- /dev/null +++ b/lib/build/recipe/oauth2provider/types.js @@ -0,0 +1,16 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/oauth2provider/utils.d.ts b/lib/build/recipe/oauth2provider/utils.d.ts new file mode 100644 index 000000000..4025b1b44 --- /dev/null +++ b/lib/build/recipe/oauth2provider/utils.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { NormalisedAppinfo } from "../../types"; +import Recipe from "./recipe"; +import { TypeInput, TypeNormalisedInput } from "./types"; +export declare function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput +): TypeNormalisedInput; diff --git a/lib/build/recipe/oauth2provider/utils.js b/lib/build/recipe/oauth2provider/utils.js new file mode 100644 index 000000000..f0bbf7edd --- /dev/null +++ b/lib/build/recipe/oauth2provider/utils.js @@ -0,0 +1,30 @@ +"use strict"; +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateAndNormaliseUserInput = void 0; +function validateAndNormaliseUserInput(_, __, config) { + let override = Object.assign( + { + functions: (originalImplementation) => originalImplementation, + apis: (originalImplementation) => originalImplementation, + }, + config === null || config === void 0 ? void 0 : config.override + ); + return { + override, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js index b308bfffb..05b9f8403 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js @@ -14,6 +14,15 @@ async function getOpenIdDiscoveryConfiguration(apiImplementation, options, userC utils_1.send200Response(options.res, { issuer: result.issuer, jwks_uri: result.jwks_uri, + authorization_endpoint: result.authorization_endpoint, + token_endpoint: result.token_endpoint, + userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, + token_introspection_endpoint: result.token_introspection_endpoint, + end_session_endpoint: result.end_session_endpoint, + subject_types_supported: result.subject_types_supported, + id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, + response_types_supported: result.response_types_supported, }); } else { utils_1.send200Response(options.res, result); diff --git a/lib/build/recipe/openid/index.d.ts b/lib/build/recipe/openid/index.d.ts index e94fd0092..84b55bd8c 100644 --- a/lib/build/recipe/openid/index.d.ts +++ b/lib/build/recipe/openid/index.d.ts @@ -8,29 +8,16 @@ export default class OpenIdRecipeWrapper { status: "OK"; issuer: string; jwks_uri: string; - }>; - static createJWT( - payload?: any, - validitySeconds?: number, - useStaticSigningKey?: boolean, - userContext?: Record - ): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - static getJWKS( - userContext?: Record - ): Promise<{ - keys: import("../jwt").JsonWebKey[]; - validityInSeconds?: number | undefined; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; } export declare let init: typeof OpenIdRecipe.init; export declare let getOpenIdDiscoveryConfiguration: typeof OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -export declare let createJWT: typeof OpenIdRecipeWrapper.createJWT; -export declare let getJWKS: typeof OpenIdRecipeWrapper.getJWKS; diff --git a/lib/build/recipe/openid/index.js b/lib/build/recipe/openid/index.js index 7fe6c9681..227ea73f7 100644 --- a/lib/build/recipe/openid/index.js +++ b/lib/build/recipe/openid/index.js @@ -5,7 +5,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getJWKS = exports.createJWT = exports.getOpenIdDiscoveryConfiguration = exports.init = void 0; +exports.getOpenIdDiscoveryConfiguration = exports.init = void 0; const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class OpenIdRecipeWrapper { @@ -14,23 +14,8 @@ class OpenIdRecipeWrapper { userContext: utils_1.getUserContext(userContext), }); } - static createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { - return recipe_1.default.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - useStaticSigningKey, - userContext: utils_1.getUserContext(userContext), - }); - } - static getJWKS(userContext) { - return recipe_1.default.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: utils_1.getUserContext(userContext), - }); - } } exports.default = OpenIdRecipeWrapper; OpenIdRecipeWrapper.init = recipe_1.default.init; exports.init = OpenIdRecipeWrapper.init; exports.getOpenIdDiscoveryConfiguration = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -exports.createJWT = OpenIdRecipeWrapper.createJWT; -exports.getJWKS = OpenIdRecipeWrapper.getJWKS; diff --git a/lib/build/recipe/openid/recipe.d.ts b/lib/build/recipe/openid/recipe.d.ts index 8091cb365..cac8551d3 100644 --- a/lib/build/recipe/openid/recipe.d.ts +++ b/lib/build/recipe/openid/recipe.d.ts @@ -5,34 +5,28 @@ import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import JWTRecipe from "../jwt/recipe"; export default class OpenIdRecipe extends RecipeModule { static RECIPE_ID: string; private static instance; config: TypeNormalisedInput; - jwtRecipe: JWTRecipe; recipeImplementation: RecipeInterface; apiImpl: APIInterface; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); + constructor(recipeId: string, appInfo: NormalisedAppinfo, config?: TypeInput); static getInstanceOrThrowError(): OpenIdRecipe; static init(config?: TypeInput): RecipeListFunction; static reset(): void; + static getIssuer(userContext: UserContext): Promise; getAPIsHandled: () => APIHandled[]; handleAPIRequest: ( id: string, - tenantId: string, + _tenantId: string, req: BaseRequest, response: BaseResponse, - path: normalisedURLPath, - method: HTTPMethod, + _path: normalisedURLPath, + _method: HTTPMethod, userContext: UserContext ) => Promise; - handleError: ( - error: STError, - request: BaseRequest, - response: BaseResponse, - userContext: UserContext - ) => Promise; + handleError: (error: STError) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; } diff --git a/lib/build/recipe/openid/recipe.js b/lib/build/recipe/openid/recipe.js index 4dc74fd25..ab6336364 100644 --- a/lib/build/recipe/openid/recipe.js +++ b/lib/build/recipe/openid/recipe.js @@ -22,7 +22,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const error_1 = __importDefault(require("../../error")); const recipeModule_1 = __importDefault(require("../../recipeModule")); const utils_1 = require("./utils"); -const recipe_1 = __importDefault(require("../jwt/recipe")); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const implementation_1 = __importDefault(require("./api/implementation")); @@ -31,7 +30,7 @@ const constants_1 = require("./constants"); const getOpenIdDiscoveryConfiguration_1 = __importDefault(require("./api/getOpenIdDiscoveryConfiguration")); const utils_2 = require("../../utils"); class OpenIdRecipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { + constructor(recipeId, appInfo, config) { super(recipeId, appInfo); this.getAPIsHandled = () => { return [ @@ -41,10 +40,9 @@ class OpenIdRecipe extends recipeModule_1.default { id: constants_1.GET_DISCOVERY_CONFIG_URL, disabled: this.apiImpl.getOpenIdDiscoveryConfigurationGET === undefined, }, - ...this.jwtRecipe.getAPIsHandled(), ]; }; - this.handleAPIRequest = async (id, tenantId, req, response, path, method, userContext) => { + this.handleAPIRequest = async (id, _tenantId, req, response, _path, _method, userContext) => { let apiOptions = { recipeImplementation: this.recipeImplementation, config: this.config, @@ -55,33 +53,20 @@ class OpenIdRecipe extends recipeModule_1.default { if (id === constants_1.GET_DISCOVERY_CONFIG_URL) { return await getOpenIdDiscoveryConfiguration_1.default(this.apiImpl, apiOptions, userContext); } else { - return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); + return false; } }; - this.handleError = async (error, request, response, userContext) => { - if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { - throw error; - } else { - return await this.jwtRecipe.handleError(error, request, response, userContext); - } + this.handleError = async (error) => { + throw error; }; this.getAllCORSHeaders = () => { - return [...this.jwtRecipe.getAllCORSHeaders()]; + return []; }; this.isErrorFromThisRecipe = (err) => { - return ( - (error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID) || - this.jwtRecipe.isErrorFromThisRecipe(err) - ); + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID; }; - this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); - this.jwtRecipe = new recipe_1.default(recipeId, appInfo, isInServerlessEnv, { - jwtValiditySeconds: this.config.jwtValiditySeconds, - override: this.config.override.jwtFeature, - }); - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(this.config, this.jwtRecipe.recipeInterfaceImpl) - ); + this.config = utils_1.validateAndNormaliseUserInput(config); + let builder = new supertokens_js_override_1.default(recipeImplementation_1.default(appInfo)); this.recipeImplementation = builder.override(this.config.override.functions).build(); let apiBuilder = new supertokens_js_override_1.default(implementation_1.default()); this.apiImpl = apiBuilder.override(this.config.override.apis).build(); @@ -93,9 +78,9 @@ class OpenIdRecipe extends recipeModule_1.default { throw new Error("Initialisation not done. Did you forget to call the Openid.init function?"); } static init(config) { - return (appInfo, isInServerlessEnv) => { + return (appInfo) => { if (OpenIdRecipe.instance === undefined) { - OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, config); return OpenIdRecipe.instance; } else { throw new Error("OpenId recipe has already been initialised. Please check your code for bugs."); @@ -108,6 +93,11 @@ class OpenIdRecipe extends recipeModule_1.default { } OpenIdRecipe.instance = undefined; } + static async getIssuer(userContext) { + return ( + await this.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }) + ).issuer; + } } exports.default = OpenIdRecipe; OpenIdRecipe.RECIPE_ID = "openid"; diff --git a/lib/build/recipe/openid/recipeImplementation.d.ts b/lib/build/recipe/openid/recipeImplementation.d.ts index d4698099c..07ebf044c 100644 --- a/lib/build/recipe/openid/recipeImplementation.d.ts +++ b/lib/build/recipe/openid/recipeImplementation.d.ts @@ -1,7 +1,4 @@ // @ts-nocheck -import { RecipeInterface, TypeNormalisedInput } from "./types"; -import { RecipeInterface as JWTRecipeInterface } from "../jwt/types"; -export default function getRecipeInterface( - config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface -): RecipeInterface; +import { RecipeInterface } from "./types"; +import { NormalisedAppinfo } from "../../types"; +export default function getRecipeInterface(appInfo: NormalisedAppinfo): RecipeInterface; diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js index 0edb22162..746c52a35 100644 --- a/lib/build/recipe/openid/recipeImplementation.js +++ b/lib/build/recipe/openid/recipeImplementation.js @@ -5,36 +5,45 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../jwt/recipe")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const constants_1 = require("../jwt/constants"); -function getRecipeInterface(config, jwtRecipeImplementation) { +const constants_2 = require("../oauth2provider/constants"); +function getRecipeInterface(appInfo) { return { getOpenIdDiscoveryConfiguration: async function () { - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); + let issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); let jwks_uri = - config.issuerDomain.getAsStringDangerous() + - config.issuerPath + appInfo.apiDomain.getAsStringDangerous() + + appInfo.apiBasePath .appendPath(new normalisedURLPath_1.default(constants_1.GET_JWKS_API)) .getAsStringDangerous(); + const apiBasePath = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); return { status: "OK", issuer, jwks_uri, + authorization_endpoint: apiBasePath + constants_2.AUTH_PATH, + token_endpoint: apiBasePath + constants_2.TOKEN_PATH, + userinfo_endpoint: apiBasePath + constants_2.USER_INFO_PATH, + revocation_endpoint: apiBasePath + constants_2.REVOKE_TOKEN_PATH, + token_introspection_endpoint: apiBasePath + constants_2.INTROSPECT_TOKEN_PATH, + end_session_endpoint: apiBasePath + constants_2.END_SESSION_PATH, + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + response_types_supported: ["code", "id_token", "id_token token"], }; }, createJWT: async function ({ payload, validitySeconds, useStaticSigningKey, userContext }) { payload = payload === undefined || payload === null ? {} : payload; - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - return await jwtRecipeImplementation.createJWT({ + let issuer = (await this.getOpenIdDiscoveryConfiguration({ userContext })).issuer; + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ payload: Object.assign({ iss: issuer }, payload), useStaticSigningKey, validitySeconds, userContext, }); }, - getJWKS: async function (input) { - return await jwtRecipeImplementation.getJWKS(input); - }, }; } exports.default = getRecipeInterface; diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts index 5b907a8d5..b0dda95fd 100644 --- a/lib/build/recipe/openid/types.d.ts +++ b/lib/build/recipe/openid/types.d.ts @@ -1,51 +1,23 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; import type { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; import { GeneralErrorResponse, UserContext } from "../../types"; export declare type TypeInput = { - issuer?: string; - jwtValiditySeconds?: number; override?: { functions?: ( originalImplementation: RecipeInterface, builder?: OverrideableBuilder ) => RecipeInterface; apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; }; }; export declare type TypeNormalisedInput = { - issuerDomain: NormalisedURLDomain; - issuerPath: NormalisedURLPath; - jwtValiditySeconds?: number; override: { functions: ( originalImplementation: RecipeInterface, builder?: OverrideableBuilder ) => RecipeInterface; apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; }; }; export declare type APIOptions = { @@ -66,6 +38,15 @@ export declare type APIInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; } | GeneralErrorResponse >); @@ -77,6 +58,15 @@ export declare type RecipeInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; createJWT(input: { payload?: any; @@ -92,9 +82,4 @@ export declare type RecipeInterface = { status: "UNSUPPORTED_ALGORITHM_ERROR"; } >; - getJWKS(input: { - userContext: UserContext; - }): Promise<{ - keys: JsonWebKey[]; - }>; }; diff --git a/lib/build/recipe/openid/utils.d.ts b/lib/build/recipe/openid/utils.d.ts index 6b5abd280..0cc991d9a 100644 --- a/lib/build/recipe/openid/utils.d.ts +++ b/lib/build/recipe/openid/utils.d.ts @@ -1,7 +1,3 @@ // @ts-nocheck -import { NormalisedAppinfo } from "../../types"; import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; +export declare function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput; diff --git a/lib/build/recipe/openid/utils.js b/lib/build/recipe/openid/utils.js index fac3a3f43..ad70f404c 100644 --- a/lib/build/recipe/openid/utils.js +++ b/lib/build/recipe/openid/utils.js @@ -1,39 +1,7 @@ "use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateAndNormaliseUserInput = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const normalisedURLDomain_1 = __importDefault(require("../../normalisedURLDomain")); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function validateAndNormaliseUserInput(appInfo, config) { - let issuerDomain = appInfo.apiDomain; - let issuerPath = appInfo.apiBasePath; - if (config !== undefined) { - if (config.issuer !== undefined) { - issuerDomain = new normalisedURLDomain_1.default(config.issuer); - issuerPath = new normalisedURLPath_1.default(config.issuer); - } - if (!issuerPath.equals(appInfo.apiBasePath)) { - throw new Error("The path of the issuer URL must be equal to the apiBasePath. The default value is /auth"); - } - } +function validateAndNormaliseUserInput(config) { let override = Object.assign( { functions: (originalImplementation) => originalImplementation, @@ -42,9 +10,6 @@ function validateAndNormaliseUserInput(appInfo, config) { config === null || config === void 0 ? void 0 : config.override ); return { - issuerDomain, - issuerPath, - jwtValiditySeconds: config === null || config === void 0 ? void 0 : config.jwtValiditySeconds, override, }; } diff --git a/lib/build/recipe/passwordless/api/consumeCode.js b/lib/build/recipe/passwordless/api/consumeCode.js index fefe0a19b..7179971cf 100644 --- a/lib/build/recipe/passwordless/api/consumeCode.js +++ b/lib/build/recipe/passwordless/api/consumeCode.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function consumeCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.consumeCodePOST === undefined) { return false; @@ -56,13 +56,11 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", }); } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -76,6 +74,7 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, } @@ -85,6 +84,7 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, } ); diff --git a/lib/build/recipe/passwordless/api/createCode.js b/lib/build/recipe/passwordless/api/createCode.js index 06b05ceb5..13617a262 100644 --- a/lib/build/recipe/passwordless/api/createCode.js +++ b/lib/build/recipe/passwordless/api/createCode.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); const max_1 = __importDefault(require("libphonenumber-js/max")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function createCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.createCodePOST === undefined) { return false; @@ -84,13 +84,11 @@ async function createCode(apiImplementation, tenantId, options, userContext) { phoneNumber = parsedPhoneNumber.format("E.164"); } } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -98,8 +96,8 @@ async function createCode(apiImplementation, tenantId, options, userContext) { } let result = await apiImplementation.createCodePOST( email !== undefined - ? { email, session, tenantId, options, userContext } - : { phoneNumber: phoneNumber, session, tenantId, options, userContext } + ? { email, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } + : { phoneNumber: phoneNumber, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } ); utils_1.send200Response(options.res, result); return true; diff --git a/lib/build/recipe/passwordless/api/implementation.js b/lib/build/recipe/passwordless/api/implementation.js index f0bcdc3ab..0936acc2c 100644 --- a/lib/build/recipe/passwordless/api/implementation.js +++ b/lib/build/recipe/passwordless/api/implementation.js @@ -178,6 +178,7 @@ function getAPIImplementation() { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { // On the frontend, this should show a UI of asking the user @@ -203,6 +204,7 @@ function getAPIImplementation() { deviceId: input.deviceId, userInputCode: input.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -210,6 +212,7 @@ function getAPIImplementation() { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -324,6 +327,7 @@ function getAPIImplementation() { factorIds, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { // On the frontend, this should show a UI of asking the user @@ -347,6 +351,7 @@ function getAPIImplementation() { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } : { @@ -360,11 +365,16 @@ function getAPIImplementation() { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } ); if (response.status !== "OK") { - return authUtils_1.AuthUtils.getErrorStatusResponseWithReason(response, {}, "SIGN_IN_UP_NOT_ALLOWED"); + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + response, + errorCodeMap, + "SIGN_IN_UP_NOT_ALLOWED" + ); } // now we send the email / text message. let magicLink = undefined; @@ -493,6 +503,7 @@ function getAPIImplementation() { ); const authTypeInfo = await authUtils_1.AuthUtils.checkAuthTypeAndLinkingStatus( input.session, + input.shouldTryLinkingWithSessionUser, { recipeId: "passwordless", email: deviceInfo.email, @@ -543,7 +554,7 @@ function getAPIImplementation() { let userInputCode = undefined; // This mirrors how we construct factorIds in createCodePOST let factorIds; - if (input.session !== undefined) { + if (!authTypeInfo.isFirstFactor) { if (deviceInfo.email !== undefined) { factorIds = [multifactorauth_1.FactorIds.OTP_EMAIL]; } else { diff --git a/lib/build/recipe/passwordless/api/resendCode.js b/lib/build/recipe/passwordless/api/resendCode.js index 419a905ac..131c04f89 100644 --- a/lib/build/recipe/passwordless/api/resendCode.js +++ b/lib/build/recipe/passwordless/api/resendCode.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function resendCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.resendCodePOST === undefined) { return false; @@ -41,23 +41,19 @@ async function resendCode(apiImplementation, tenantId, options, userContext) { message: "Please provide a deviceId", }); } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { - tenantId = session.getTenantId(); - } let result = await apiImplementation.resendCodePOST({ deviceId, preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/passwordless/index.js b/lib/build/recipe/passwordless/index.js index 74774d368..285ccb9ed 100644 --- a/lib/build/recipe/passwordless/index.js +++ b/lib/build/recipe/passwordless/index.js @@ -29,6 +29,7 @@ class Wrapper { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( Object.assign(Object.assign({}, input), { session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: utils_1.getUserContext(input.userContext), }) ); @@ -44,6 +45,7 @@ class Wrapper { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( Object.assign(Object.assign({}, input), { session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: utils_1.getUserContext(input.userContext), }) ); diff --git a/lib/build/recipe/passwordless/recipe.js b/lib/build/recipe/passwordless/recipe.js index 09294ae9f..6ff155b8b 100644 --- a/lib/build/recipe/passwordless/recipe.js +++ b/lib/build/recipe/passwordless/recipe.js @@ -141,6 +141,7 @@ class Recipe extends recipeModule_1.default { email: input.email, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -148,6 +149,7 @@ class Recipe extends recipeModule_1.default { phoneNumber: input.phoneNumber, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -180,12 +182,14 @@ class Recipe extends recipeModule_1.default { email: input.email, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } : { phoneNumber: input.phoneNumber, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } ); @@ -198,6 +202,7 @@ class Recipe extends recipeModule_1.default { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -206,6 +211,7 @@ class Recipe extends recipeModule_1.default { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } diff --git a/lib/build/recipe/passwordless/recipeImplementation.js b/lib/build/recipe/passwordless/recipeImplementation.js index d156a7e2e..d2fb36376 100644 --- a/lib/build/recipe/passwordless/recipeImplementation.js +++ b/lib/build/recipe/passwordless/recipeImplementation.js @@ -39,12 +39,13 @@ function getRecipeInterface(querier) { response.recipeUserId = new recipeUserId_1.default(response.recipeUserId); // Attempt account linking (this is a sign up) let updatedUser = response.user; - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId: input.tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, userContext: input.userContext, } ); @@ -174,6 +175,7 @@ function getRecipeInterface(querier) { let response = await querier.sendPutRequest( new normalisedURLPath_1.default(`/recipe/user`), copyAndRemoveUserContextAndTenantId(input), + {}, input.userContext ); if (response.status !== "OK") { diff --git a/lib/build/recipe/passwordless/types.d.ts b/lib/build/recipe/passwordless/types.d.ts index 2535a78f6..434f7902e 100644 --- a/lib/build/recipe/passwordless/types.d.ts +++ b/lib/build/recipe/passwordless/types.d.ts @@ -92,6 +92,7 @@ export declare type RecipeInterface = { ) & { userInputCode?: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -132,6 +133,7 @@ export declare type RecipeInterface = { deviceId: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -139,6 +141,7 @@ export declare type RecipeInterface = { linkCode: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -305,6 +308,7 @@ export declare type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -328,6 +332,7 @@ export declare type APIInterface = { } & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -351,6 +356,7 @@ export declare type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } diff --git a/lib/build/recipe/session/accessToken.js b/lib/build/recipe/session/accessToken.js index 3f0efd2cf..37249db05 100644 --- a/lib/build/recipe/session/accessToken.js +++ b/lib/build/recipe/session/accessToken.js @@ -199,6 +199,9 @@ async function getInfoFromAccessToken(jwtInfo, jwks, doAntiCsrfCheck) { } exports.getInfoFromAccessToken = getInfoFromAccessToken; function validateAccessTokenStructure(payload, version) { + if (payload.stt !== 0 && payload.stt !== undefined) { + throw Error("Wrong token type"); + } if (version >= 5) { if ( typeof payload.sub !== "string" || diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index 4aa6e6b05..84d5b0225 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -30,4 +30,5 @@ exports.protectedProps = [ "antiCsrfToken", "rsub", "tId", + "stt", ]; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts index 55fbe988b..8115d4727 100644 --- a/lib/build/recipe/session/index.d.ts +++ b/lib/build/recipe/session/index.d.ts @@ -170,6 +170,7 @@ export default class SessionWrapper { userContext?: Record ): Promise<{ keys: import("../jwt").JsonWebKey[]; + validityInSeconds?: number | undefined; }>; static getOpenIdDiscoveryConfiguration( userContext?: Record @@ -177,6 +178,15 @@ export default class SessionWrapper { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; static fetchAndSetClaim( sessionHandle: string, diff --git a/lib/build/recipe/session/index.js b/lib/build/recipe/session/index.js index 63f8d33be..eebfd546a 100644 --- a/lib/build/recipe/session/index.js +++ b/lib/build/recipe/session/index.js @@ -22,6 +22,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getOpenIdDiscoveryConfiguration = exports.getJWKS = exports.createJWT = exports.Error = exports.validateClaimsForSessionHandle = exports.removeClaim = exports.getClaimValue = exports.setClaimValue = exports.fetchAndSetClaim = exports.mergeIntoAccessTokenPayload = exports.updateSessionDataInDatabase = exports.revokeMultipleSessions = exports.revokeSession = exports.getAllSessionHandlesForUser = exports.revokeAllSessionsForUser = exports.refreshSessionWithoutRequestResponse = exports.refreshSession = exports.getSessionInformation = exports.getSessionWithoutRequestResponse = exports.getSession = exports.createNewSessionWithoutRequestResponse = exports.createNewSession = exports.init = void 0; const error_1 = __importDefault(require("./error")); const recipe_1 = __importDefault(require("./recipe")); +const recipe_2 = __importDefault(require("../openid/recipe")); +const recipe_3 = __importDefault(require("../jwt/recipe")); const utils_1 = require("./utils"); const sessionRequestFunctions_1 = require("./sessionRequestFunctions"); const __1 = require("../.."); @@ -71,8 +73,7 @@ class SessionWrapper { const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const appInfo = recipeInstance.getAppInfo(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + const issuer = await recipe_2.default.getIssuer(ctx); let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); for (const prop of constants_2.protectedProps) { delete finalAccessTokenPayload[prop]; @@ -249,7 +250,7 @@ class SessionWrapper { }); } static createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { - return recipe_1.default.getInstanceOrThrowError().openIdRecipe.recipeImplementation.createJWT({ + return recipe_2.default.getInstanceOrThrowError().recipeImplementation.createJWT({ payload, validitySeconds, useStaticSigningKey, @@ -257,16 +258,14 @@ class SessionWrapper { }); } static getJWKS(userContext) { - return recipe_1.default.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ + return recipe_3.default.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ userContext: utils_2.getUserContext(userContext), }); } static getOpenIdDiscoveryConfiguration(userContext) { - return recipe_1.default - .getInstanceOrThrowError() - .openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext: utils_2.getUserContext(userContext), - }); + return recipe_2.default.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ + userContext: utils_2.getUserContext(userContext), + }); } static fetchAndSetClaim(sessionHandle, claim, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 0489b1e81..ecb163142 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -13,7 +13,6 @@ import STError from "./error"; import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod, UserContext } from "../../types"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; -import OpenIdRecipe from "../openid/recipe"; export default class SessionRecipe extends RecipeModule { private static instance; static RECIPE_ID: string; @@ -21,7 +20,6 @@ export default class SessionRecipe extends RecipeModule { private claimValidatorsAddedByOtherRecipes; config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; - openIdRecipe: OpenIdRecipe; apiImpl: APIInterface; isInServerlessEnv: boolean; constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); @@ -35,11 +33,11 @@ export default class SessionRecipe extends RecipeModule { getAPIsHandled: () => APIHandled[]; handleAPIRequest: ( id: string, - tenantId: string, + _tenantId: string, req: BaseRequest, res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod, + _path: NormalisedURLPath, + _method: HTTPMethod, userContext: UserContext ) => Promise; handleError: ( @@ -56,4 +54,5 @@ export default class SessionRecipe extends RecipeModule { response: BaseResponse, userContext: UserContext ) => Promise; + getNormalisedOverwriteSessionDuringSignInUp: (req: any) => boolean; } diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index 429fbb415..fcec65ac9 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -31,8 +31,8 @@ const recipeImplementation_1 = __importDefault(require("./recipeImplementation") const querier_1 = require("../../querier"); const implementation_1 = __importDefault(require("./api/implementation")); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const recipe_1 = __importDefault(require("../openid/recipe")); const logger_1 = require("../../logger"); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); const utils_2 = require("../../utils"); // For Express class SessionRecipe extends recipeModule_1.default { @@ -74,10 +74,9 @@ class SessionRecipe extends recipeModule_1.default { disabled: this.apiImpl.signOutPOST === undefined, }, ]; - apisHandled.push(...this.openIdRecipe.getAPIsHandled()); return apisHandled; }; - this.handleAPIRequest = async (id, tenantId, req, res, path, method, userContext) => { + this.handleAPIRequest = async (id, _tenantId, req, res, _path, _method, userContext) => { let options = { config: this.config, recipeId: this.getRecipeId(), @@ -91,7 +90,7 @@ class SessionRecipe extends recipeModule_1.default { } else if (id === constants_1.SIGNOUT_API_PATH) { return await signout_1.default(this.apiImpl, options, userContext); } else { - return await this.openIdRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); + return false; } }; this.handleError = async (err, request, response, userContext) => { @@ -155,19 +154,15 @@ class SessionRecipe extends recipeModule_1.default { throw err; } } else { - return await this.openIdRecipe.handleError(err, request, response, userContext); + throw err; } }; this.getAllCORSHeaders = () => { let corsHeaders = [...cookieAndHeaders_1.getCORSAllowedHeaders()]; - corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()); return corsHeaders; }; this.isErrorFromThisRecipe = (err) => { - return ( - error_1.default.isErrorFromSuperTokens(err) && - (err.fromRecipe === SessionRecipe.RECIPE_ID || this.openIdRecipe.isErrorFromThisRecipe(err)) - ); + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === SessionRecipe.RECIPE_ID; }; this.verifySession = async (options, request, response, userContext) => { return await this.apiImpl.verifySession({ @@ -183,6 +178,14 @@ class SessionRecipe extends recipeModule_1.default { userContext, }); }; + this.getNormalisedOverwriteSessionDuringSignInUp = (req) => { + var _a; + const supportsFDI31 = utils_2.hasGreaterThanEqualToFDI(req, "3.1"); + const res = + (_a = this.config.overwriteSessionDuringSignInUp) !== null && _a !== void 0 ? _a : supportsFDI31; + logger_1.logDebugMessage("getNormalisedOverwriteSessionDuringSignInUp returning: " + res); + return res; + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); const antiCsrfToLog = typeof this.config.antiCsrfFunctionOrString === "string" @@ -199,9 +202,6 @@ class SessionRecipe extends recipeModule_1.default { ); logger_1.logDebugMessage("session init: sessionExpiredStatusCode: " + this.config.sessionExpiredStatusCode); this.isInServerlessEnv = isInServerlessEnv; - this.openIdRecipe = new recipe_1.default(recipeId, appInfo, isInServerlessEnv, { - override: this.config.override.openIdFeature, - }); let builder = new supertokens_js_override_1.default( recipeImplementation_1.default( querier_1.Querier.getNewInstanceOrThrowError(recipeId), @@ -239,6 +239,7 @@ class SessionRecipe extends recipeModule_1.default { throw new Error("calling testing function in non testing env"); } SessionRecipe.instance = undefined; + combinedRemoteJWKSet_1.resetCombinedJWKS(); } } exports.default = SessionRecipe; diff --git a/lib/build/recipe/session/recipeImplementation.d.ts b/lib/build/recipe/session/recipeImplementation.d.ts index 9c1969e84..df95edba8 100644 --- a/lib/build/recipe/session/recipeImplementation.d.ts +++ b/lib/build/recipe/session/recipeImplementation.d.ts @@ -1,11 +1,9 @@ // @ts-nocheck -import { JWTVerifyGetKey } from "jose"; import { RecipeInterface, TypeNormalisedInput } from "./types"; import { Querier } from "../../querier"; import { NormalisedAppinfo } from "../../types"; export declare type Helpers = { querier: Querier; - JWKS: JWTVerifyGetKey; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; getRecipeImpl: () => RecipeInterface; diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js index 0250d0376..7dbba30ec 100644 --- a/lib/build/recipe/session/recipeImplementation.js +++ b/lib/build/recipe/session/recipeImplementation.js @@ -41,7 +41,6 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const jose_1 = require("jose"); const SessionFunctions = __importStar(require("./sessionFunctions")); const cookieAndHeaders_1 = require("./cookieAndHeaders"); const utils_1 = require("./utils"); @@ -56,36 +55,6 @@ const constants_1 = require("../multitenancy/constants"); const constants_2 = require("./constants"); const utils_2 = require("../../utils"); function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) { - const JWKS = querier.getAllCoreUrlsForPath("/.well-known/jwks.json").map((url) => - jose_1.createRemoteJWKSet(new URL(url), { - cooldownDuration: constants_2.JWKCacheCooldownInMs, - cacheMaxAge: config.jwksRefreshIntervalSec * 1000, - }) - ); - /** - This function fetches all JWKs from the first available core instance. This combines the other JWKS functions to become - error resistant. - - Every core instance a backend is connected to is expected to connect to the same database and use the same key set for - token verification. Otherwise, the result of session verification would depend on which core is currently available. - */ - const combinedJWKS = async (...args) => { - let lastError = undefined; - if (JWKS.length === 0) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - for (const jwks of JWKS) { - try { - // We await before returning to make sure we catch the error - return await jwks(...args); - } catch (ex) { - lastError = ex; - } - } - throw lastError; - }; let obj = { createNewSession: async function ({ recipeUserId, @@ -438,7 +407,6 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride }; let helpers = { querier, - JWKS: combinedJWKS, config, appInfo, getRecipeImpl: getRecipeImplAfterOverrides, diff --git a/lib/build/recipe/session/sessionFunctions.js b/lib/build/recipe/session/sessionFunctions.js index 2e522a2c8..f939f0367 100644 --- a/lib/build/recipe/session/sessionFunctions.js +++ b/lib/build/recipe/session/sessionFunctions.js @@ -28,6 +28,7 @@ const utils_1 = require("../../utils"); const logger_1 = require("../../logger"); const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); /** * @description call this to "login" a user. */ @@ -98,7 +99,7 @@ async function getSession( */ accessTokenInfo = await accessToken_1.getInfoFromAccessToken( parsedAccessToken, - helpers.JWKS, + combinedRemoteJWKSet_1.getCombinedJWKS(config), helpers.config.antiCsrfFunctionOrString === "VIA_TOKEN" && doAntiCsrfCheck ); } catch (err) { @@ -460,6 +461,7 @@ async function updateSessionDataInDatabase(helpers, sessionHandle, newSessionDat sessionHandle, userDataInDatabase: newSessionData, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { @@ -477,6 +479,7 @@ async function updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPa sessionHandle, userDataInJWT: newAccessTokenPayload, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { diff --git a/lib/build/recipe/session/sessionRequestFunctions.d.ts b/lib/build/recipe/session/sessionRequestFunctions.d.ts index 7204931a1..3002f3bd2 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.d.ts +++ b/lib/build/recipe/session/sessionRequestFunctions.d.ts @@ -1,6 +1,13 @@ // @ts-nocheck import Recipe from "./recipe"; -import { VerifySessionOptions, RecipeInterface, TypeNormalisedInput, SessionContainerInterface } from "./types"; +import { + VerifySessionOptions, + RecipeInterface, + TokenTransferMethod, + TypeNormalisedInput, + SessionContainerInterface, +} from "./types"; +import { ParsedJWTInfo } from "./jwt"; import { NormalisedAppinfo, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare function getSessionFromRequest({ @@ -18,6 +25,14 @@ export declare function getSessionFromRequest({ options?: VerifySessionOptions; userContext: UserContext; }): Promise; +export declare function getAccessTokenFromRequest( + req: any, + allowedTransferMethod: TokenTransferMethod | "any" +): { + requestTransferMethod: TokenTransferMethod | undefined; + accessToken: ParsedJWTInfo | undefined; + allowedTransferMethod: TokenTransferMethod | "any"; +}; export declare function refreshSessionInRequest({ res, req, diff --git a/lib/build/recipe/session/sessionRequestFunctions.js b/lib/build/recipe/session/sessionRequestFunctions.js index 1d33897a7..df0a56f0e 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.js +++ b/lib/build/recipe/session/sessionRequestFunctions.js @@ -5,9 +5,10 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createNewSessionInRequest = exports.refreshSessionInRequest = exports.getSessionFromRequest = void 0; +exports.createNewSessionInRequest = exports.refreshSessionInRequest = exports.getAccessTokenFromRequest = exports.getSessionFromRequest = void 0; const framework_1 = __importDefault(require("../../framework")); const supertokens_1 = __importDefault(require("../../supertokens")); +const recipe_1 = __importDefault(require("../openid/recipe")); const utils_1 = require("./utils"); const utils_2 = require("../../utils"); const logger_1 = require("../../logger"); @@ -44,58 +45,12 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op } const sessionOptional = (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; logger_1.logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens = {}; - // We check all token transfer methods for available access tokens - for (const transferMethod of constants_1.availableTokenTransferMethods) { - const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); - accessToken_1.validateAccessTokenStructure(info.payload, info.version); - logger_1.logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch (_a) { - logger_1.logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); - let requestTransferMethod; - let accessToken; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using cookie transfer method"); - // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. - // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). - // ensuring outdated token payload isn't used. - const hasMultipleAccessTokenCookies = cookieAndHeaders_1.hasMultipleCookiesForTokenType(req, "access"); - if (hasMultipleAccessTokenCookies) { - logger_1.logDebugMessage( - "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" - ); - throw new error_1.default({ - message: "Multiple access tokens present in the request cookies.", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } + const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(req, allowedTransferMethod); let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; if (doAntiCsrfCheck === undefined) { @@ -168,6 +123,57 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op return session; } exports.getSessionFromRequest = getSessionFromRequest; +function getAccessTokenFromRequest(req, allowedTransferMethod) { + const accessTokens = {}; + // We check all token transfer methods for available access tokens + for (const transferMethod of constants_1.availableTokenTransferMethods) { + const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); + if (tokenString !== undefined) { + try { + const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); + accessToken_1.validateAccessTokenStructure(info.payload, info.version); + logger_1.logDebugMessage("getSession: got access token from " + transferMethod); + accessTokens[transferMethod] = info; + } catch (_a) { + logger_1.logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` + ); + } + } + } + let requestTransferMethod; + let accessToken; + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + accessTokens["header"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using header transfer method"); + requestTransferMethod = "header"; + accessToken = accessTokens["header"]; + } else if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && + accessTokens["cookie"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using cookie transfer method"); + // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. + // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). + // ensuring outdated token payload isn't used. + const hasMultipleAccessTokenCookies = cookieAndHeaders_1.hasMultipleCookiesForTokenType(req, "access"); + if (hasMultipleAccessTokenCookies) { + logger_1.logDebugMessage( + "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" + ); + throw new error_1.default({ + message: "Multiple access tokens present in the request cookies.", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } + requestTransferMethod = "cookie"; + accessToken = accessTokens["cookie"]; + } + return { requestTransferMethod, accessToken, allowedTransferMethod }; +} +exports.getAccessTokenFromRequest = getAccessTokenFromRequest; /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours @@ -378,7 +384,7 @@ async function createNewSessionInRequest({ logger_1.logDebugMessage("createNewSession: Wrapping done"); userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + const issuer = await recipe_1.default.getIssuer(userContext); let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); for (const prop of constants_1.protectedProps) { delete finalAccessTokenPayload[prop]; diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index 1a06349bf..b573925ed 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -1,9 +1,7 @@ // @ts-nocheck import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; -import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; @@ -64,26 +62,6 @@ export declare type TypeInput = { builder?: OverrideableBuilder ) => RecipeInterface; apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; }; }; export declare type TypeNormalisedInput = { @@ -99,7 +77,7 @@ export declare type TypeNormalisedInput = { cookieSecure: boolean; sessionExpiredStatusCode: number; errorHandlers: NormalisedErrorHandlers; - overwriteSessionDuringSignInUp: boolean; + overwriteSessionDuringSignInUp: boolean | undefined; antiCsrfFunctionOrString: | "VIA_TOKEN" | "VIA_CUSTOM_HEADER" @@ -119,26 +97,6 @@ export declare type TypeNormalisedInput = { builder?: OverrideableBuilder ) => RecipeInterface; apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; }; }; export interface SessionRequest extends BaseRequest { diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js index e8f0af0b1..e4bfc3e2b 100644 --- a/lib/build/recipe/session/utils.js +++ b/lib/build/recipe/session/utils.js @@ -89,7 +89,7 @@ function getURLProtocol(url) { } exports.getURLProtocol = getURLProtocol; function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { - var _a, _b, _c, _d, _e; + var _a, _b, _c, _d; let cookieDomain = config === undefined || config.cookieDomain === undefined ? undefined @@ -231,14 +231,11 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { override, invalidClaimStatusCode, overwriteSessionDuringSignInUp: - (_d = config === null || config === void 0 ? void 0 : config.overwriteSessionDuringSignInUp) !== null && + config === null || config === void 0 ? void 0 : config.overwriteSessionDuringSignInUp, + jwksRefreshIntervalSec: + (_d = config === null || config === void 0 ? void 0 : config.jwksRefreshIntervalSec) !== null && _d !== void 0 ? _d - : false, - jwksRefreshIntervalSec: - (_e = config === null || config === void 0 ? void 0 : config.jwksRefreshIntervalSec) !== null && - _e !== void 0 - ? _e : 3600 * 4, }; } diff --git a/lib/build/recipe/thirdparty/api/implementation.js b/lib/build/recipe/thirdparty/api/implementation.js index a47368aff..1fed7ca95 100644 --- a/lib/build/recipe/thirdparty/api/implementation.js +++ b/lib/build/recipe/thirdparty/api/implementation.js @@ -129,6 +129,7 @@ function getAPIInterface() { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { logger_1.logDebugMessage( @@ -150,6 +151,7 @@ function getAPIInterface() { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId, userContext, }); diff --git a/lib/build/recipe/thirdparty/api/signinup.js b/lib/build/recipe/thirdparty/api/signinup.js index 027fcc4d7..772fd4bcd 100644 --- a/lib/build/recipe/thirdparty/api/signinup.js +++ b/lib/build/recipe/thirdparty/api/signinup.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const error_1 = __importDefault(require("../error")); const utils_1 = require("../../../utils"); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signInUpAPI(apiImplementation, tenantId, options, userContext) { if (apiImplementation.signInUpPOST === undefined) { return false; @@ -66,13 +66,14 @@ async function signInUpAPI(apiImplementation, tenantId, options, userContext) { }); } const provider = providerResponse; - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + bodyParams + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -84,6 +85,7 @@ async function signInUpAPI(apiImplementation, tenantId, options, userContext) { oAuthTokens, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/thirdparty/index.js b/lib/build/recipe/thirdparty/index.js index 6e8aadbe4..2ee060ce9 100644 --- a/lib/build/recipe/thirdparty/index.js +++ b/lib/build/recipe/thirdparty/index.js @@ -49,6 +49,7 @@ class Wrapper { tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, isVerified, session, + shouldTryLinkingWithSessionUser: !!session, userContext: utils_1.getUserContext(userContext), }); } diff --git a/lib/build/recipe/thirdparty/providers/bitbucket.js b/lib/build/recipe/thirdparty/providers/bitbucket.js index 27588fc51..6c5a60d23 100644 --- a/lib/build/recipe/thirdparty/providers/bitbucket.js +++ b/lib/build/recipe/thirdparty/providers/bitbucket.js @@ -19,7 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const custom_1 = __importDefault(require("./custom")); const logger_1 = require("../../../logger"); function Bitbucket(input) { @@ -59,7 +59,7 @@ function Bitbucket(input) { fromUserInfoAPI: {}, fromIdTokenPayload: {}, }; - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( "https://api.bitbucket.org/2.0/user", undefined, headers @@ -73,7 +73,7 @@ function Bitbucket(input) { ); } rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; - const userInfoFromEmail = await utils_1.doGetRequest( + const userInfoFromEmail = await thirdpartyUtils_1.doGetRequest( "https://api.bitbucket.org/2.0/user/emails", undefined, headers diff --git a/lib/build/recipe/thirdparty/providers/custom.js b/lib/build/recipe/thirdparty/providers/custom.js index 3aead6ffc..ddd2b7a00 100644 --- a/lib/build/recipe/thirdparty/providers/custom.js +++ b/lib/build/recipe/thirdparty/providers/custom.js @@ -6,7 +6,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getActualClientIdFromDevelopmentClientId = exports.isUsingDevelopmentClientId = exports.DEV_OAUTH_REDIRECT_URL = void 0; -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const pkce_challenge_1 = __importDefault(require("pkce-challenge")); const configUtils_1 = require("./configUtils"); const jose_1 = require("jose"); @@ -251,7 +251,7 @@ function NewProvider(input) { accessTokenAPIParams["redirect_uri"] = exports.DEV_OAUTH_REDIRECT_URL; } /* Transformation needed for dev keys END */ - const tokenResponse = await utils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); + const tokenResponse = await thirdpartyUtils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); if (tokenResponse.status >= 400) { logger_1.logDebugMessage( `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` @@ -273,7 +273,7 @@ function NewProvider(input) { if (jwks === undefined) { jwks = jose_1.createRemoteJWKSet(new URL(impl.config.jwksURI)); } - rawUserInfoFromProvider.fromIdTokenPayload = await utils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + rawUserInfoFromProvider.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( idToken, jwks, { @@ -318,7 +318,7 @@ function NewProvider(input) { } } } - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( impl.config.userInfoEndpoint, queryParams, headers diff --git a/lib/build/recipe/thirdparty/providers/github.js b/lib/build/recipe/thirdparty/providers/github.js index a1d6e2e13..ced201b08 100644 --- a/lib/build/recipe/thirdparty/providers/github.js +++ b/lib/build/recipe/thirdparty/providers/github.js @@ -21,7 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const utils_1 = require("../../../utils"); const custom_1 = __importDefault(require("./custom")); -const utils_2 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoResponse) { if (rawUserInfoResponse.fromUserInfoAPI === undefined) { throw new Error("rawUserInfoResponse.fromUserInfoAPI is not available"); @@ -59,7 +59,7 @@ function Github(input) { const basicAuthToken = utils_1.encodeBase64( `${clientConfig.clientId}:${clientConfig.clientSecret === undefined ? "" : clientConfig.clientSecret}` ); - const applicationResponse = await utils_2.doPostRequest( + const applicationResponse = await thirdpartyUtils_1.doPostRequest( `https://api.github.com/applications/${clientConfig.clientId}/token`, { access_token: accessToken, @@ -96,14 +96,22 @@ function Github(input) { Accept: "application/vnd.github.v3+json", }; const rawResponse = {}; - const emailInfoResp = await utils_2.doGetRequest("https://api.github.com/user/emails", undefined, headers); + const emailInfoResp = await thirdpartyUtils_1.doGetRequest( + "https://api.github.com/user/emails", + undefined, + headers + ); if (emailInfoResp.status >= 400) { throw new Error( `Getting userInfo failed with ${emailInfoResp.status}: ${emailInfoResp.stringResponse}` ); } rawResponse.emails = emailInfoResp.jsonResponse; - const userInfoResp = await utils_2.doGetRequest("https://api.github.com/user", undefined, headers); + const userInfoResp = await thirdpartyUtils_1.doGetRequest( + "https://api.github.com/user", + undefined, + headers + ); if (userInfoResp.status >= 400) { throw new Error(`Getting userInfo failed with ${userInfoResp.status}: ${userInfoResp.stringResponse}`); } diff --git a/lib/build/recipe/thirdparty/providers/linkedin.js b/lib/build/recipe/thirdparty/providers/linkedin.js index bce0eeaf4..defa0739c 100644 --- a/lib/build/recipe/thirdparty/providers/linkedin.js +++ b/lib/build/recipe/thirdparty/providers/linkedin.js @@ -21,7 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const logger_1 = require("../../../logger"); const custom_1 = __importDefault(require("./custom")); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function Linkedin(input) { if (input.config.name === undefined) { input.config.name = "LinkedIn"; @@ -56,7 +56,7 @@ function Linkedin(input) { fromIdTokenPayload: {}, }; // https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin%2Fconsumer%2Fcontext#sample-api-response - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( "https://api.linkedin.com/v2/userinfo", undefined, headers diff --git a/lib/build/recipe/thirdparty/providers/twitter.js b/lib/build/recipe/thirdparty/providers/twitter.js index a901c40d6..ec82e5638 100644 --- a/lib/build/recipe/thirdparty/providers/twitter.js +++ b/lib/build/recipe/thirdparty/providers/twitter.js @@ -53,7 +53,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("../../../logger"); const utils_1 = require("../../../utils"); const custom_1 = __importStar(require("./custom")); -const utils_2 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function Twitter(input) { var _a; if (input.config.name === undefined) { @@ -114,7 +114,7 @@ function Twitter(input) { }, originalImplementation.config.tokenEndpointBodyParams ); - const tokenResponse = await utils_2.doPostRequest( + const tokenResponse = await thirdpartyUtils_1.doPostRequest( originalImplementation.config.tokenEndpoint, twitterOauthTokenParams, { diff --git a/lib/build/recipe/thirdparty/providers/utils.js b/lib/build/recipe/thirdparty/providers/utils.js index dc4e1128f..bacd7b02f 100644 --- a/lib/build/recipe/thirdparty/providers/utils.js +++ b/lib/build/recipe/thirdparty/providers/utils.js @@ -43,6 +43,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); exports.normaliseOIDCEndpointToIncludeWellKnown = exports.discoverOIDCEndpoints = exports.verifyIdTokenFromJWKSEndpointAndGetPayload = exports.doPostRequest = exports.doGetRequest = void 0; const jose = __importStar(require("jose")); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const logger_1 = require("../../../logger"); @@ -105,27 +106,9 @@ async function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOp return payload; } exports.verifyIdTokenFromJWKSEndpointAndGetPayload = verifyIdTokenFromJWKSEndpointAndGetPayload; -// OIDC utils -var oidcInfoMap = {}; -async function getOIDCDiscoveryInfo(issuer) { - if (oidcInfoMap[issuer] !== undefined) { - return oidcInfoMap[issuer]; - } - const normalizedDomain = new normalisedURLDomain_1.default(issuer); - const normalizedPath = new normalisedURLPath_1.default(issuer); - let oidcInfo = await doGetRequest(normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous()); - if (oidcInfo.status > 400) { - logger_1.logDebugMessage( - `Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}` - ); - throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); - } - oidcInfoMap[issuer] = oidcInfo.jsonResponse; - return oidcInfo.jsonResponse; -} async function discoverOIDCEndpoints(config) { if (config.oidcDiscoveryEndpoint !== undefined) { - const oidcInfo = await getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); + const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); if (oidcInfo.authorization_endpoint !== undefined && config.authorizationEndpoint === undefined) { config.authorizationEndpoint = oidcInfo.authorization_endpoint; } diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js index 0e8ee80f1..dce4ef1e6 100644 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ b/lib/build/recipe/thirdparty/recipeImplementation.js @@ -23,6 +23,7 @@ function getRecipeImplementation(querier, providers) { isVerified, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, }) { const accountLinking = recipe_1.default.getInstance(); @@ -73,9 +74,10 @@ function getRecipeImplementation(querier, providers) { // we do this so that we get the updated user (in case the above // function updated the verification status) and can return that response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, + shouldTryLinkingWithSessionUser, inputUser: response.user, recipeUserId: response.recipeUserId, session, @@ -101,6 +103,7 @@ function getRecipeImplementation(querier, providers) { userContext, oAuthTokens, session, + shouldTryLinkingWithSessionUser, rawUserInfoFromProvider, }) { let response = await this.manuallyCreateOrUpdateUser({ @@ -110,6 +113,7 @@ function getRecipeImplementation(querier, providers) { tenantId, isVerified, session, + shouldTryLinkingWithSessionUser, userContext, }); if (response.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { diff --git a/lib/build/recipe/thirdparty/types.d.ts b/lib/build/recipe/thirdparty/types.d.ts index 041656d62..8d0198e3b 100644 --- a/lib/build/recipe/thirdparty/types.d.ts +++ b/lib/build/recipe/thirdparty/types.d.ts @@ -175,6 +175,7 @@ export declare type RecipeInterface = { }; }; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -214,6 +215,7 @@ export declare type RecipeInterface = { email: string; isVerified: boolean; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -275,6 +277,7 @@ export declare type APIInterface = { provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/build/recipe/totp/recipeImplementation.js b/lib/build/recipe/totp/recipeImplementation.js index 5465f884c..5ad44874b 100644 --- a/lib/build/recipe/totp/recipeImplementation.js +++ b/lib/build/recipe/totp/recipeImplementation.js @@ -100,6 +100,7 @@ function getRecipeInterface(querier, config) { existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, }, + {}, input.userContext ); }, diff --git a/lib/build/recipe/usermetadata/recipeImplementation.js b/lib/build/recipe/usermetadata/recipeImplementation.js index e3aa25a5f..2fd78898b 100644 --- a/lib/build/recipe/usermetadata/recipeImplementation.js +++ b/lib/build/recipe/usermetadata/recipeImplementation.js @@ -36,6 +36,7 @@ function getRecipeInterface(querier) { userId, metadataUpdate, }, + {}, userContext ); }, diff --git a/lib/build/recipe/userroles/recipe.js b/lib/build/recipe/userroles/recipe.js index fec355e1e..3f16cf7be 100644 --- a/lib/build/recipe/userroles/recipe.js +++ b/lib/build/recipe/userroles/recipe.js @@ -27,8 +27,10 @@ const utils_1 = require("./utils"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); const recipe_1 = __importDefault(require("../session/recipe")); +const recipe_2 = __importDefault(require("../oauth2provider/recipe")); const userRoleClaim_1 = require("./userRoleClaim"); const permissionClaim_1 = require("./permissionClaim"); +const session_1 = require("../session"); const utils_2 = require("../../utils"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { @@ -52,6 +54,81 @@ class Recipe extends recipeModule_1.default { if (!this.config.skipAddingPermissionsToAccessToken) { recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(permissionClaim_1.PermissionClaim); } + const tokenPayloadBuilder = async (user, scopes, sessionHandle, userContext) => { + let payload = {}; + const sessionInfo = await session_1.getSessionInformation(sessionHandle, userContext); + let userRoles = []; + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId: sessionInfo.tenantId, + userContext, + }); + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + if (scopes.includes("roles")) { + payload.roles = userRoles; + } + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + payload.permissions = Array.from(userPermissions); + } + return payload; + }; + recipe_2.default.getInstanceOrThrowError().addAccessTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + recipe_2.default.getInstanceOrThrowError().addIdTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + recipe_2.default + .getInstanceOrThrowError() + .addUserInfoBuilderFromOtherRecipe(async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo = {}; + let userRoles = []; + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + if (scopes.includes("roles")) { + userInfo.roles = userRoles; + } + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + userInfo.permissions = Array.from(userPermissions); + } + return userInfo; + }); }); } /* Init functions */ diff --git a/lib/build/recipe/userroles/recipeImplementation.js b/lib/build/recipe/userroles/recipeImplementation.js index ca12893f9..47dbdc1ba 100644 --- a/lib/build/recipe/userroles/recipeImplementation.js +++ b/lib/build/recipe/userroles/recipeImplementation.js @@ -29,6 +29,7 @@ function getRecipeInterface(querier) { `/${tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId}/recipe/user/role` ), { userId, role }, + {}, userContext ); }, @@ -63,6 +64,7 @@ function getRecipeInterface(querier) { return querier.sendPutRequest( new normalisedURLPath_1.default("/recipe/role"), { role, permissions }, + {}, userContext ); }, diff --git a/lib/build/recipeModule.d.ts b/lib/build/recipeModule.d.ts index 778cd14ee..e277192a9 100644 --- a/lib/build/recipeModule.d.ts +++ b/lib/build/recipeModule.d.ts @@ -5,7 +5,7 @@ import NormalisedURLPath from "./normalisedURLPath"; import { BaseRequest, BaseResponse } from "./framework"; export default abstract class RecipeModule { private recipeId; - private appInfo; + protected appInfo: NormalisedAppinfo; constructor(recipeId: string, appInfo: NormalisedAppinfo); getRecipeId: () => string; getAppInfo: () => NormalisedAppinfo; @@ -17,6 +17,7 @@ export default abstract class RecipeModule { | { id: string; tenantId: string; + exactMatch: boolean; } | undefined >; diff --git a/lib/build/recipeModule.js b/lib/build/recipeModule.js index 7b3eef889..75c3219ae 100644 --- a/lib/build/recipeModule.js +++ b/lib/build/recipeModule.js @@ -54,7 +54,7 @@ class RecipeModule { tenantIdFromFrontend: constants_1.DEFAULT_TENANT_ID, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: true }; } else if ( remainingPath !== undefined && this.appInfo.apiBasePath @@ -65,7 +65,7 @@ class RecipeModule { tenantIdFromFrontend: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: false }; } } } diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js index ddc2aafeb..db7ceb9ef 100644 --- a/lib/build/supertokens.js +++ b/lib/build/supertokens.js @@ -135,6 +135,7 @@ class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, + {}, input.userContext ); } else { @@ -162,6 +163,7 @@ class SuperTokens { requestRID = undefined; } async function handleWithoutRid(recipeModules) { + let bestMatch = undefined; for (let i = 0; i < recipeModules.length; i++) { logger_1.logDebugMessage( "middleware: Checking recipe ID for match: " + @@ -173,26 +175,40 @@ class SuperTokens { ); let idResult = await recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (idResult !== undefined) { - logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); - let requestHandled = await recipeModules[i].handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext - ); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + if (bestMatch === undefined || idResult.exactMatch) { + bestMatch = { + recipeModule: recipeModules[i], + idResult: idResult, + }; + } + if (idResult.exactMatch) { + break; } - logger_1.logDebugMessage("middleware: Ended"); - return true; } } + if (bestMatch !== undefined) { + const { idResult, recipeModule } = bestMatch; + logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + let requestHandled = await recipeModule.handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, + path, + method, + userContext + ); + if (!requestHandled) { + logger_1.logDebugMessage( + "middleware: Not handled because API returned requestHandled as false" + ); + return false; + } + logger_1.logDebugMessage("middleware: Ended"); + return true; + } logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); return false; } @@ -238,13 +254,18 @@ class SuperTokens { // the path and methods of the APIs exposed via those recipes is unique. let currIdResult = await matchedRecipe[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (currIdResult !== undefined) { - if (idResult !== undefined) { + if ( + idResult === undefined || + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + (currIdResult.exactMatch === true && idResult.exactMatch === false) + ) { + finalMatchedRecipe = matchedRecipe[i]; + idResult = currIdResult; + } else { throw new Error( "Two recipes have matched the same API path and method! This is a bug in the SDK. Please contact support." ); - } else { - finalMatchedRecipe = matchedRecipe[i]; - idResult = currIdResult; } } } @@ -355,6 +376,9 @@ class SuperTokens { let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; + let oauth2Found = false; + let openIdFound = false; + let jwtFound = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way // so that there is no circular dependency. Otherwise there would be cyclic dependency // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` @@ -362,6 +386,9 @@ class SuperTokens { let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; let TotpRecipe = require("./recipe/totp/recipe").default; + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; + let OpenIdRecipe = require("./recipe/openid/recipe").default; + let jwtRecipe = require("./recipe/jwt/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); if (recipeModule.getRecipeId() === MultitenancyRecipe.RECIPE_ID) { @@ -372,9 +399,21 @@ class SuperTokens { multiFactorAuthFound = true; } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { totpFound = true; + } else if (recipeModule.getRecipeId() === OAuth2ProviderRecipe.RECIPE_ID) { + oauth2Found = true; + } else if (recipeModule.getRecipeId() === OpenIdRecipe.RECIPE_ID) { + openIdFound = true; + } else if (recipeModule.getRecipeId() === jwtRecipe.RECIPE_ID) { + jwtFound = true; } return recipeModule; }); + if (!jwtFound) { + this.recipeModules.push(jwtRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } + if (!openIdFound) { + this.recipeModules.push(OpenIdRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } if (!multitenancyFound) { this.recipeModules.push(MultitenancyRecipe.init()(this.appInfo, this.isInServerlessEnv)); } @@ -390,6 +429,10 @@ class SuperTokens { // the app doesn't have to do that if they only use TOTP (which shouldn't be that uncommon) // To let those cases function without initializing account linking we do not check it here, but when // the authentication endpoints are called. + // We've decided to always initialize the OAuth2Provider recipe + if (!oauth2Found) { + this.recipeModules.push(OAuth2ProviderRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } this.telemetryEnabled = config.telemetry === undefined ? !utils_1.isTestEnv() : config.telemetry; } static init(config) { @@ -402,6 +445,15 @@ class SuperTokens { if (!utils_1.isTestEnv()) { throw new Error("calling testing function in non testing env"); } + // We call reset the following recipes because they are auto-initialized + // and there is no case where we want to reset the SuperTokens instance but not + // the recipes. + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; + OAuth2ProviderRecipe.reset(); + let OpenIdRecipe = require("./recipe/openid/recipe").default; + OpenIdRecipe.reset(); + let JWTRecipe = require("./recipe/jwt/recipe").default; + JWTRecipe.reset(); querier_1.Querier.reset(); SuperTokens.instance = undefined; } diff --git a/lib/build/thirdpartyUtils.d.ts b/lib/build/thirdpartyUtils.d.ts new file mode 100644 index 000000000..84517d5e2 --- /dev/null +++ b/lib/build/thirdpartyUtils.d.ts @@ -0,0 +1,34 @@ +// @ts-nocheck +import * as jose from "jose"; +export declare function doGetRequest( + url: string, + queryParams?: { + [key: string]: string; + }, + headers?: { + [key: string]: string; + } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}>; +export declare function doPostRequest( + url: string, + params: { + [key: string]: any; + }, + headers?: { + [key: string]: string; + } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}>; +export declare function verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken: string, + jwks: jose.JWTVerifyGetKey, + otherOptions: jose.JWTVerifyOptions +): Promise; +export declare function getOIDCDiscoveryInfo(issuer: string): Promise; diff --git a/lib/build/thirdpartyUtils.js b/lib/build/thirdpartyUtils.js new file mode 100644 index 000000000..83cc3d405 --- /dev/null +++ b/lib/build/thirdpartyUtils.js @@ -0,0 +1,128 @@ +"use strict"; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getOIDCDiscoveryInfo = exports.verifyIdTokenFromJWKSEndpointAndGetPayload = exports.doPostRequest = exports.doGetRequest = void 0; +const jose = __importStar(require("jose")); +const logger_1 = require("./logger"); +const utils_1 = require("./utils"); +const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); +const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); +async function doGetRequest(url, queryParams, headers) { + logger_1.logDebugMessage( + `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` + ); + if ((headers === null || headers === void 0 ? void 0 : headers["Accept"]) === undefined) { + headers = Object.assign(Object.assign({}, headers), { Accept: "application/json" }); + } + const finalURL = new URL(url); + finalURL.search = new URLSearchParams(queryParams).toString(); + let response = await utils_1.doFetch(finalURL.toString(), { + headers: headers, + }); + const stringResponse = await response.text(); + let jsonResponse = undefined; + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} +exports.doGetRequest = doGetRequest; +async function doPostRequest(url, params, headers) { + if (headers === undefined) { + headers = {}; + } + headers["Content-Type"] = "application/x-www-form-urlencoded"; + headers["Accept"] = "application/json"; + logger_1.logDebugMessage( + `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` + ); + const body = new URLSearchParams(params).toString(); + let response = await utils_1.doFetch(url, { + method: "POST", + body, + headers, + }); + const stringResponse = await response.text(); + let jsonResponse = undefined; + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} +exports.doPostRequest = doPostRequest; +async function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOptions) { + const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); + return payload; +} +exports.verifyIdTokenFromJWKSEndpointAndGetPayload = verifyIdTokenFromJWKSEndpointAndGetPayload; +// OIDC utils +var oidcInfoMap = {}; +async function getOIDCDiscoveryInfo(issuer) { + const normalizedDomain = new normalisedURLDomain_1.default(issuer); + let normalizedPath = new normalisedURLPath_1.default(issuer); + if (oidcInfoMap[issuer] !== undefined) { + return oidcInfoMap[issuer]; + } + const oidcInfo = await doGetRequest( + normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() + ); + if (oidcInfo.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}` + ); + throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + } + oidcInfoMap[issuer] = oidcInfo.jsonResponse; + return oidcInfo.jsonResponse; +} +exports.getOIDCDiscoveryInfo = getOIDCDiscoveryInfo; diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index 4ee670175..c45e6de40 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -10,6 +10,9 @@ declare type Brand = { [__brand]: B; }; declare type Branded = T & Brand; +export declare type NonNullableProperties = { + [P in keyof T]: NonNullable; +}; export declare type UserContext = Branded, "UserContext">; export declare type AppInfo = { appName: string; @@ -62,7 +65,7 @@ export declare type APIHandled = { id: string; disabled: boolean; }; -export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "patch" | "options" | "trace"; export declare type JSONPrimitive = string | number | boolean | null; export declare type JSONArray = Array; export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined; diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index e16cbed34..b681642e1 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -12,6 +12,7 @@ export declare function sendNon200ResponseWithMessage(res: BaseResponse, message export declare function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject): void; export declare function send200Response(res: BaseResponse, responseJson: any): void; export declare function isAnIpAddress(ipaddress: string): boolean; +export declare function getNormalisedShouldTryLinkingWithSessionUserFlag(req: BaseRequest, body: any): any; export declare function getBackwardsCompatibleUserInfo( req: BaseRequest, result: { @@ -57,6 +58,14 @@ export declare function postWithFetch( } >; export declare function normaliseEmail(email: string): string; +export declare function toCamelCase(str: string): string; +export declare function toSnakeCase(str: string): string; +export declare function transformObjectKeys( + obj: { + [key: string]: any; + }, + caseType: "snake-case" | "camelCase" +): T; export declare const getProcess: () => any; export declare const getBuffer: () => any; export declare const isTestEnv: () => boolean; diff --git a/lib/build/utils.js b/lib/build/utils.js index 1363e2355..0f0ddadde 100644 --- a/lib/build/utils.js +++ b/lib/build/utils.js @@ -5,7 +5,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.isBuffer = exports.decodeBase64 = exports.encodeBase64 = exports.isTestEnv = exports.getBuffer = exports.getProcess = exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.getUserContext = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.hasGreaterThanEqualToFDI = exports.getLatestFDIVersionFromFDIList = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; +exports.isBuffer = exports.decodeBase64 = exports.encodeBase64 = exports.isTestEnv = exports.getBuffer = exports.getProcess = exports.transformObjectKeys = exports.toSnakeCase = exports.toCamelCase = exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.getUserContext = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.hasGreaterThanEqualToFDI = exports.getLatestFDIVersionFromFDIList = exports.getBackwardsCompatibleUserInfo = exports.getNormalisedShouldTryLinkingWithSessionUserFlag = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; const tldts_1 = require("tldts"); const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); @@ -22,6 +22,7 @@ const doFetch = async (input, init) => { ); init = { cache: "no-cache", + redirect: "manual", }; } else { if (init.cache === undefined) { @@ -29,6 +30,7 @@ const doFetch = async (input, init) => { processState_1.PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH ); init.cache = "no-cache"; + init.redirect = "manual"; } } const fetchFunction = typeof fetch !== "undefined" ? fetch : cross_fetch_1.default; @@ -186,6 +188,14 @@ function isAnIpAddress(ipaddress) { ); } exports.isAnIpAddress = isAnIpAddress; +function getNormalisedShouldTryLinkingWithSessionUserFlag(req, body) { + var _a; + if (hasGreaterThanEqualToFDI(req, "3.1")) { + return (_a = body.shouldTryLinkingWithSessionUser) !== null && _a !== void 0 ? _a : false; + } + return undefined; +} +exports.getNormalisedShouldTryLinkingWithSessionUserFlag = getNormalisedShouldTryLinkingWithSessionUserFlag; function getBackwardsCompatibleUserInfo(req, result, userContext) { let resp; // (>= 1.18 && < 2.0) || >= 3.0: This is because before 1.18, and between 2 and 3, FDI does not @@ -399,6 +409,26 @@ function normaliseEmail(email) { return email; } exports.normaliseEmail = normaliseEmail; +function toCamelCase(str) { + return str.replace(/([-_][a-z])/gi, (match) => { + return match.toUpperCase().replace("-", "").replace("_", ""); + }); +} +exports.toCamelCase = toCamelCase; +function toSnakeCase(str) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} +exports.toSnakeCase = toSnakeCase; +// Transforms the keys of an object from camelCase to snakeCase or vice versa. +function transformObjectKeys(obj, caseType) { + const transformKey = caseType === "camelCase" ? toCamelCase : toSnakeCase; + return Object.entries(obj).reduce((result, [key, value]) => { + const transformedKey = transformKey(key); + result[transformedKey] = value; + return result; + }, {}); +} +exports.transformObjectKeys = transformObjectKeys; const getProcess = () => { /** * Return the process instance if it is available falling back diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index c788f9605..e151552ba 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "20.1.3"; +export declare const version = "21.0.0"; export declare const cdiSupported: string[]; export declare const dashboardVersion = "0.13"; diff --git a/lib/build/version.js b/lib/build/version.js index 828d08498..5fab53774 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "20.1.3"; -exports.cdiSupported = ["5.1"]; +exports.version = "21.0.0"; +exports.cdiSupported = ["5.2"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.13"; diff --git a/lib/ts/authUtils.ts b/lib/ts/authUtils.ts index 0443941bf..ae278847c 100644 --- a/lib/ts/authUtils.ts +++ b/lib/ts/authUtils.ts @@ -10,7 +10,7 @@ import RecipeUserId from "./recipeUserId"; import { updateAndGetMFARelatedInfoInSession } from "./recipe/multifactorauth/utils"; import { isValidFirstFactor } from "./recipe/multitenancy/utils"; import SessionError from "./recipe/session/error"; -import { getUser } from "."; +import { Error as STError, getUser } from "."; import { AccountInfoWithRecipeId } from "./recipe/accountlinking/types"; import { BaseRequest, BaseResponse } from "./framework"; import SessionRecipe from "./recipe/session/recipe"; @@ -82,6 +82,7 @@ export const AuthUtils = { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }: { authenticatingAccountInfo: AccountInfoWithRecipeId; @@ -93,6 +94,7 @@ export const AuthUtils = { signInVerifiesLoginMethod: boolean; skipSessionUserUpdateInCore: boolean; session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }): Promise< | { status: "OK"; validFactorIds: string[]; isFirstFactor: boolean } @@ -118,6 +120,7 @@ export const AuthUtils = { // We also load the session user here if it is available. const authTypeInfo = await AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authenticatingAccountInfo, authenticatingUser, skipSessionUserUpdateInCore, @@ -277,8 +280,9 @@ export const AuthUtils = { // If the new user wasn't linked to the current one, we check the config and overwrite the session if required // Note: we could also get here if MFA is enabled, but the app didn't want to link the user to the session user. // This is intentional, since the MFA and overwriteSessionDuringSignInUp configs should work independently. - let overwriteSessionDuringSignInUp = SessionRecipe.getInstanceOrThrowError().config - .overwriteSessionDuringSignInUp; + let overwriteSessionDuringSignInUp = SessionRecipe.getInstanceOrThrowError().getNormalisedOverwriteSessionDuringSignInUp( + req + ); if (overwriteSessionDuringSignInUp) { respSession = await Session.createNewSession(req, res, tenantId, recipeUserId, {}, {}, userContext); if (mfaInstance !== undefined) { @@ -287,6 +291,9 @@ export const AuthUtils = { } } } else { + // We do not have to care about overwriting the session here, since we either: + // - have overwriteSessionDuringSignInUp true and we didn't even try to load the session because we ignore it anyway + // - have overwriteSessionDuringSignInUp false and we checked in the api imlp that there is no session logDebugMessage(`postAuthChecks creating session for first factor sign in/up`); // If there is no input session, we do not need to do anything other checks and create a new session respSession = await Session.createNewSession(req, res, tenantId, recipeUserId, {}, {}, userContext); @@ -480,6 +487,7 @@ export const AuthUtils = { */ checkAuthTypeAndLinkingStatus: async function ( session: SessionContainerInterface | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined, accountInfo: AccountInfoWithRecipeId, inputUser: User | undefined, skipSessionUserUpdateInCore: boolean, @@ -503,20 +511,51 @@ export const AuthUtils = { logDebugMessage(`checkAuthTypeAndLinkingStatus called`); let sessionUser: User | undefined = undefined; if (session === undefined) { + if (shouldTryLinkingWithSessionUser === true) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session not found but shouldTryLinkingWithSessionUser is true", + }); + } logDebugMessage(`checkAuthTypeAndLinkingStatus returning first factor because there is no session`); // If there is no active session we have nothing to link to - so this has to be a first factor sign in return { status: "OK", isFirstFactor: true }; } else { + if (shouldTryLinkingWithSessionUser === false) { + logDebugMessage( + `checkAuthTypeAndLinkingStatus returning first factor because shouldTryLinkingWithSessionUser is false` + ); + // In our normal flows this should never happen - but some user overrides might do this. + // Anyway, since shouldTryLinkingWithSessionUser explicitly set to false, it's safe to consider this a firstFactor + return { status: "OK", isFirstFactor: true }; + } + if (!recipeInitDefinedShouldDoAutomaticAccountLinking(AccountLinking.getInstance().config)) { - if (MultiFactorAuthRecipe.getInstance() !== undefined) { + if (shouldTryLinkingWithSessionUser === true) { throw new Error( "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" ); } else { - return { status: "OK", isFirstFactor: true }; + // This is the legacy case where shouldTryLinkingWithSessionUser is undefined + if (MultiFactorAuthRecipe.getInstance() !== undefined) { + throw new Error( + "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" + ); + } else { + logDebugMessage( + `checkAuthTypeAndLinkingStatus (legacy behaviour) returning first factor because MFA is not initialised and shouldDoAutomaticAccountLinking is not defined` + ); + return { status: "OK", isFirstFactor: true }; + } } } + // If we get here: + // - session is defined + // - shouldTryLinkingWithSessionUser is true or undefined + // - shouldDoAutomaticAccountLinking is defined + // - MFA may or may not be initialized + // If the input and the session user are the same if (inputUser !== undefined && inputUser.id === session.getUserId()) { logDebugMessage( @@ -542,6 +581,15 @@ export const AuthUtils = { userContext ); if (sessionUserResult.status === "SHOULD_AUTOMATICALLY_LINK_FALSE") { + if (shouldTryLinkingWithSessionUser === true) { + // tryAndMakeSessionUserIntoAPrimaryUser throws if it is an email verification iss + throw new STError({ + message: + "shouldDoAutomaticAccountLinking returned false when making the session user primary but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } + return { status: "OK", isFirstFactor: true, @@ -572,6 +620,13 @@ export const AuthUtils = { ); if (shouldLink.shouldAutomaticallyLink === false) { + if (shouldTryLinkingWithSessionUser === true) { + throw new STError({ + message: + "shouldDoAutomaticAccountLinking returned false when making the session user primary but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true }; } else { return { @@ -599,17 +654,19 @@ export const AuthUtils = { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }: { tenantId: string; inputUser: User; recipeUserId: RecipeUserId; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }): Promise< | { status: "OK"; user: User } @@ -622,13 +679,14 @@ export const AuthUtils = { | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } > { - logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo called"); + logDebugMessage("linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo called"); const retry = () => { - logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); - return AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + logDebugMessage("linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); + return AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: inputUser, session, + shouldTryLinkingWithSessionUser, recipeUserId, userContext, }); @@ -647,6 +705,7 @@ export const AuthUtils = { const authTypeRes = await AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authLoginMethod, inputUser, false, @@ -660,12 +719,12 @@ export const AuthUtils = { if (authTypeRes.isFirstFactor) { if (!recipeInitDefinedShouldDoAutomaticAccountLinking(AccountLinking.getInstance().config)) { logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo skipping link by account info because this is a first factor auth and the app hasn't defined shouldDoAutomaticAccountLinking" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo skipping link by account info because this is a first factor auth and the app hasn't defined shouldDoAutomaticAccountLinking" ); return { status: "OK", user: inputUser }; } logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by account info because this is a first factor auth" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by account info because this is a first factor auth" ); // We try and list all users that can be linked to the input user based on the account info // later we can use these when trying to link or when checking if linking to the session user is possible. @@ -692,7 +751,7 @@ export const AuthUtils = { } logDebugMessage( - "linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by session info" + "linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo trying to link by session info" ); const sessionLinkingRes = await AuthUtils.tryLinkingBySession({ sessionUser: authTypeRes.sessionUser, @@ -959,6 +1018,53 @@ export const AuthUtils = { return validFactorIds; }, + loadSessionInAuthAPIIfNeeded: async function ( + req: BaseRequest, + res: BaseResponse, + shouldTryLinkingWithSessionUser: boolean | undefined, + userContext: UserContext + ) { + const overwriteSessionDuringSignInUp = SessionRecipe.getInstanceOrThrowError().getNormalisedOverwriteSessionDuringSignInUp( + req + ); + + if (shouldTryLinkingWithSessionUser !== false) { + logDebugMessage( + "loadSessionInAuthAPIIfNeeded: loading session because shouldTryLinkingWithSessionUser is not set to false so we may want to link later" + ); + return await Session.getSession( + req, + res, + { + // This is optional only if shouldTryLinkingWithSessionUser is undefined + // in the (old) 3.0 FDI, this flag didn't exist and we linking was based on the session presence and shouldDoAutomaticAccountLinking + sessionRequired: shouldTryLinkingWithSessionUser === true, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + } + + if (overwriteSessionDuringSignInUp === false) { + logDebugMessage( + "loadSessionInAuthAPIIfNeeded: loading session in optional mode because overwriteSessionDuringSignInUp is false so if it is not found we will skip session creation" + ); + return await Session.getSession( + req, + res, + { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + } + logDebugMessage( + "loadSessionInAuthAPIIfNeeded: skipping session loading because we are not linking and we would overwrite it anyway" + ); + + return undefined; + }, }; async function filterOutInvalidSecondFactorsOrThrowIfAllAreInvalid( diff --git a/lib/ts/combinedRemoteJWKSet.ts b/lib/ts/combinedRemoteJWKSet.ts new file mode 100644 index 000000000..063b243ac --- /dev/null +++ b/lib/ts/combinedRemoteJWKSet.ts @@ -0,0 +1,55 @@ +import { createRemoteJWKSet } from "jose"; +import { JWKCacheCooldownInMs } from "./recipe/session/constants"; +import { Querier } from "./querier"; + +let combinedJWKS: ReturnType | undefined; + +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +export function resetCombinedJWKS() { + combinedJWKS = undefined; +} + +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +export function getCombinedJWKS(config: { jwksRefreshIntervalSec: number }) { + if (combinedJWKS === undefined) { + const JWKS: ReturnType[] = Querier.getNewInstanceOrThrowError(undefined) + .getAllCoreUrlsForPath("/.well-known/jwks.json") + .map((url) => + createRemoteJWKSet(new URL(url), { + cacheMaxAge: config.jwksRefreshIntervalSec * 1000, + cooldownDuration: JWKCacheCooldownInMs, + }) + ); + + combinedJWKS = async (...args) => { + let lastError = undefined; + + if (JWKS.length === 0) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + for (const jwks of JWKS) { + try { + // We await before returning to make sure we catch the error + return await jwks(...args); + } catch (ex) { + lastError = ex; + } + } + throw lastError; + }; + } + return combinedJWKS; +} diff --git a/lib/ts/framework/request.ts b/lib/ts/framework/request.ts index 7e24b9f3b..5bac2548f 100644 --- a/lib/ts/framework/request.ts +++ b/lib/ts/framework/request.ts @@ -63,4 +63,26 @@ export abstract class BaseRequest { } return this.parsedJSONBody; }; + + getBodyAsJSONOrFormData = async (): Promise => { + const contentType = this.getHeaderValue("content-type"); + + if (contentType) { + if (contentType.startsWith("application/json")) { + return await this.getJSONBody(); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + return await this.getFormData(); + } + } else { + try { + return await this.getJSONBody(); + } catch { + try { + return await this.getFormData(); + } catch { + throw new Error("Unable to parse body as JSON or Form Data."); + } + } + } + }; } diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 21f872cd9..1c8af9b2e 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -163,8 +163,9 @@ export class Querier { let apiVersion = await this.getAPIVersion(userContext); let headers: any = { "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", }; + headers["content-type"] = "application/json; charset=utf-8"; + if (Querier.apiKey !== undefined) { headers = { ...headers, @@ -344,12 +345,15 @@ export class Querier { finalURL.search = searchParams.toString(); // Update cache and return - let response = await doFetch(finalURL.toString(), { method: "GET", headers, }); + if (response.status === 302) { + return response; + } + if (response.status === 200 && !Querier.disableCache) { // If the request was successful, we save the result into the cache // plus we update the cache tag @@ -374,6 +378,7 @@ export class Querier { sendGetRequestWithResponseHeaders = async ( path: NormalisedURLPath, params: Record, + inpHeaders: Record | undefined, userContext: UserContext ): Promise<{ body: any; headers: Headers }> => { return await this.sendRequestHelper( @@ -381,7 +386,9 @@ export class Querier { "GET", async (url: string) => { let apiVersion = await this.getAPIVersion(userContext); - let headers: any = { "cdi-version": apiVersion }; + let headers: any = inpHeaders ?? {}; + headers["cdi-version"] = apiVersion; + if (Querier.apiKey !== undefined) { headers = { ...headers, @@ -425,7 +432,12 @@ export class Querier { }; // path should start with "/" - sendPutRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + sendPutRequest = async ( + path: NormalisedURLPath, + body: any, + params: Record, + userContext: UserContext + ): Promise => { this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( @@ -453,6 +465,7 @@ export class Querier { method: "put", headers: headers, body: body, + params: params, }, userContext ); @@ -463,7 +476,13 @@ export class Querier { } } - return doFetch(url, { + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) as string[][] + ); + finalURL.search = searchParams.toString(); + + return doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -474,6 +493,56 @@ export class Querier { return respBody; }; + // path should start with "/" + sendPatchRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + this.invalidateCoreCallCache(userContext); + + const { body: respBody } = await this.sendRequestHelper( + path, + "PATCH", + async (url: string) => { + let apiVersion = await this.getAPIVersion(userContext); + let headers: any = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + "api-key": Querier.apiKey, + }; + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + }; + } + if (Querier.networkInterceptor !== undefined) { + let request = Querier.networkInterceptor( + { + url: url, + method: "patch", + headers: headers, + body: body, + }, + userContext + ); + url = request.url; + headers = request.headers; + if (request.body !== undefined) { + body = request.body; + } + } + + return doFetch(url, { + method: "PATCH", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + this.__hosts?.length || 0 + ); + return respBody; + }; + invalidateCoreCallCache = (userContext: UserContext, updGlobalCacheTagIfNecessary = true) => { if (updGlobalCacheTagIfNecessary && userContext._default?.keepCacheAlive !== true) { Querier.globalCacheTag = Date.now(); @@ -518,7 +587,10 @@ export class Querier { } let currentDomain: string = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); let currentBasePath: string = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - const url = currentDomain + currentBasePath + path.getAsStringDangerous(); + + let strPath = path.getAsStringDangerous(); + + const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; if (retryInfoMap === undefined) { @@ -538,6 +610,7 @@ export class Querier { if (isTestEnv()) { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } + if (response.status !== 200) { throw response; } diff --git a/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts b/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts index 16a6cd2a9..c57cc7b0d 100644 --- a/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts +++ b/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts @@ -18,7 +18,7 @@ import MultitenancyRecipe from "../../../multitenancy/recipe"; import { UserContext } from "../../../../types"; import NormalisedURLDomain from "../../../../normalisedURLDomain"; import NormalisedURLPath from "../../../../normalisedURLPath"; -import { doPostRequest } from "../../../thirdparty/providers/utils"; +import { doPostRequest } from "../../../../thirdpartyUtils"; import { DEFAULT_TENANT_ID } from "../../../multitenancy/constants"; import { encodeBase64 } from "../../../../utils"; diff --git a/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts b/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts index 4c0870ac3..96585bfc4 100644 --- a/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts +++ b/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts @@ -23,7 +23,7 @@ import { ProviderConfig } from "../../../thirdparty/types"; import { UserContext } from "../../../../types"; import NormalisedURLDomain from "../../../../normalisedURLDomain"; import NormalisedURLPath from "../../../../normalisedURLPath"; -import { doGetRequest } from "../../../thirdparty/providers/utils"; +import { doGetRequest } from "../../../../thirdpartyUtils"; export type Response = | { diff --git a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts index fb600bb05..85da1aca2 100644 --- a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts +++ b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts @@ -35,7 +35,7 @@ export default async function generatePasswordResetToken( // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, requestBody.formFields, diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 7467c2957..635ff2bd6 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -1,6 +1,5 @@ import { APIInterface, APIOptions } from "../"; import { logDebugMessage } from "../../../logger"; -import { SessionContainerInterface } from "../../session/types"; import { GeneralErrorResponse, User, UserContext } from "../../../types"; import { getUser } from "../../../"; import AccountLinking from "../../accountlinking/recipe"; @@ -10,6 +9,7 @@ import RecipeUserId from "../../../recipeUserId"; import { getPasswordResetLink } from "../utils"; import { AuthUtils } from "../../../authUtils"; import { isFakeEmail } from "../../thirdparty/utils"; +import { SessionContainerInterface } from "../../session/types"; export default function getAPIImplementation(): APIInterface { return { @@ -65,7 +65,15 @@ export default function getAPIImplementation(): APIInterface { | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { - const email = formFields.filter((f) => f.id === "email")[0].value; + // NOTE: Check for email being a non-string value. This check will likely + // never evaluate to `true` as there is an upper-level check for the type + // in validation but kept here to be safe. + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + const email: string = emailAsUnknown; // this function will be reused in different parts of the flow below.. async function generateAndSendPasswordResetToken( @@ -322,7 +330,7 @@ export default function getAPIImplementation(): APIInterface { }: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -451,7 +459,15 @@ export default function getAPIImplementation(): APIInterface { } } - let newPassword = formFields.filter((f) => f.id === "password")[0].value; + // NOTE: Check for password being a non-string value. This check will likely + // never evaluate to `true` as there is an upper-level check for the type + // in validation but kept here to be safe. + const newPasswordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + if (typeof newPasswordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + let newPassword: string = newPasswordAsUnknown; let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, @@ -472,7 +488,7 @@ export default function getAPIImplementation(): APIInterface { // This should happen only cause of a race condition where the user // might be deleted before token creation and consumption. // Also note that this being undefined doesn't mean that the email password - // user does not exist, but it means that there is no reicpe or primary user + // user does not exist, but it means that there is no recipe or primary user // for whom the token was generated. return { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", @@ -591,15 +607,17 @@ export default function getAPIImplementation(): APIInterface { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }): Promise< @@ -631,8 +649,24 @@ export default function getAPIImplementation(): APIInterface { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_012)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + + if (typeof passwordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; const recipeId = "emailpassword"; @@ -683,6 +717,7 @@ export default function getAPIImplementation(): APIInterface { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status === "SIGN_UP_NOT_ALLOWED") { throw new Error("This should never happen: pre-auth checks should not fail for sign in"); @@ -702,6 +737,7 @@ export default function getAPIImplementation(): APIInterface { email, password, session, + shouldTryLinkingWithSessionUser, tenantId, userContext, }); @@ -740,15 +776,17 @@ export default function getAPIImplementation(): APIInterface { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }): Promise< @@ -780,8 +818,24 @@ export default function getAPIImplementation(): APIInterface { "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_016)", }, }; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (typeof emailAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the email value is a string in validateFormFieldsOrThrowError" + ); + + if (typeof passwordAsUnknown !== "string") + throw new Error( + "Should never come here since we already check that the password value is a string in validateFormFieldsOrThrowError" + ); + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; const preAuthCheckRes = await AuthUtils.preAuthChecks({ authenticatingAccountInfo: { @@ -797,6 +851,7 @@ export default function getAPIImplementation(): APIInterface { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthCheckRes.status === "SIGN_UP_NOT_ALLOWED") { @@ -834,6 +889,7 @@ export default function getAPIImplementation(): APIInterface { email, password, session, + shouldTryLinkingWithSessionUser, userContext, }); diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts index c6a26c010..9ab19df6b 100644 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ b/lib/ts/recipe/emailpassword/api/passwordReset.ts @@ -39,7 +39,7 @@ export default async function passwordReset( // a password that meets the password policy. let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, requestBody.formFields, diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts index 77c1161ff..899d06bc0 100644 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ b/lib/ts/recipe/emailpassword/api/signin.ts @@ -13,11 +13,15 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signInAPI( apiImplementation: APIInterface, @@ -30,24 +34,24 @@ export default async function signInAPI( return false; } + const body = await options.req.getJSONBody(); // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.signInFeature.formFields, - (await options.req.getJSONBody()).formFields, + body.formFields, tenantId, userContext ); - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -59,6 +63,7 @@ export default async function signInAPI( formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts index ec9a2781d..bb4c1efa2 100644 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ b/lib/ts/recipe/emailpassword/api/signup.ts @@ -13,12 +13,16 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import STError from "../error"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signUpAPI( apiImplementation: APIInterface, @@ -37,7 +41,7 @@ export default async function signUpAPI( // step 1 let formFields: { id: string; - value: string; + value: unknown; }[] = await validateFormFieldsOrThrowError( options.config.signUpFeature.formFields, requestBody.formFields, @@ -45,16 +49,14 @@ export default async function signUpAPI( userContext ); - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, requestBody); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { tenantId = session.getTenantId(); } @@ -63,6 +65,7 @@ export default async function signUpAPI( formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext: userContext, }); diff --git a/lib/ts/recipe/emailpassword/api/utils.ts b/lib/ts/recipe/emailpassword/api/utils.ts index 23023ae1f..71204187d 100644 --- a/lib/ts/recipe/emailpassword/api/utils.ts +++ b/lib/ts/recipe/emailpassword/api/utils.ts @@ -25,7 +25,7 @@ export async function validateFormFieldsOrThrowError( ): Promise< { id: string; - value: string; + value: unknown; }[] > { // first we check syntax ---------------------------- @@ -39,7 +39,7 @@ export async function validateFormFieldsOrThrowError( let formFields: { id: string; - value: string; + value: unknown; }[] = []; for (let i = 0; i < formFieldsRaw.length; i++) { @@ -52,7 +52,7 @@ export async function validateFormFieldsOrThrowError( } if (curr.id === FORM_FIELD_EMAIL_ID || curr.id === FORM_FIELD_PASSWORD_ID) { if (typeof curr.value !== "string") { - throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); + throw newBadRequestError(`${curr.id} value must be a string`); } } formFields.push(curr); @@ -63,7 +63,7 @@ export async function validateFormFieldsOrThrowError( if (field.id === FORM_FIELD_EMAIL_ID) { return { ...field, - value: field.value.trim(), + value: typeof field.value === "string" ? field.value.trim() : field.value, }; } return field; diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index e654d16b4..d5b071b51 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -91,6 +91,7 @@ export default class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: getUserContext(userContext), }); @@ -143,6 +144,7 @@ export default class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: getUserContext(userContext), }); diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts index 4c2e39d70..1c883021c 100644 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ b/lib/ts/recipe/emailpassword/recipeImplementation.ts @@ -18,7 +18,7 @@ export default function getRecipeInterface( return { signUp: async function ( this: RecipeInterface, - { email, password, tenantId, session, userContext } + { email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext } ): Promise< | { status: "OK"; @@ -47,11 +47,12 @@ export default function getRecipeInterface( let updatedUser = response.user; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }); @@ -103,7 +104,10 @@ export default function getRecipeInterface( // users are always initially unverified. }, - signIn: async function (this: RecipeInterface, { email, password, tenantId, session, userContext }) { + signIn: async function ( + this: RecipeInterface, + { email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext } + ) { const response = await this.verifyCredentials({ email, password, tenantId, userContext }); if (response.status === "OK") { @@ -133,11 +137,12 @@ export default function getRecipeInterface( response.user = (await getUser(response.recipeUserId!.getAsString(), userContext))!; } - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }); if (linkResult.status === "LINKING_TO_SESSION_USER_FAILED") { @@ -320,6 +325,7 @@ export default function getRecipeInterface( email: input.email, password: input.password, }, + {}, input.userContext ); diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index ee8b481df..0fdfbe088 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -88,6 +88,7 @@ export type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -128,6 +129,7 @@ export type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -228,7 +230,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; options: APIOptions; @@ -249,7 +251,7 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; token: string; tenantId: string; @@ -273,10 +275,11 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< @@ -300,10 +303,11 @@ export type APIInterface = { | ((input: { formFields: { id: string; - value: string; + value: unknown; }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< diff --git a/lib/ts/recipe/jwt/recipeImplementation.ts b/lib/ts/recipe/jwt/recipeImplementation.ts index dc8656124..fa937c881 100644 --- a/lib/ts/recipe/jwt/recipeImplementation.ts +++ b/lib/ts/recipe/jwt/recipeImplementation.ts @@ -78,6 +78,7 @@ export default function getRecipeInterface( const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new NormalisedURLPath("/.well-known/jwks.json"), {}, + undefined, userContext ); let validityInSeconds = defaultJWKSMaxAge; diff --git a/lib/ts/recipe/multitenancy/recipeImplementation.ts b/lib/ts/recipe/multitenancy/recipeImplementation.ts index 48316c97e..d3694647a 100644 --- a/lib/ts/recipe/multitenancy/recipeImplementation.ts +++ b/lib/ts/recipe/multitenancy/recipeImplementation.ts @@ -16,6 +16,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { tenantId, ...config, }, + {}, userContext ); @@ -68,6 +69,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { config, skipValidation, }, + {}, userContext ); return response; diff --git a/lib/ts/recipe/oauth2client/api/implementation.ts b/lib/ts/recipe/oauth2client/api/implementation.ts new file mode 100644 index 000000000..613b81937 --- /dev/null +++ b/lib/ts/recipe/oauth2client/api/implementation.ts @@ -0,0 +1,72 @@ +import { APIInterface } from "../"; +import Session from "../../session"; +import { OAuthTokens } from "../types"; + +export default function getAPIInterface(): APIInterface { + return { + signInPOST: async function (input) { + const { options, tenantId, userContext, clientId } = input; + + let normalisedClientId = clientId; + if (normalisedClientId === undefined) { + if (options.config.providerConfigs.length > 1) { + throw new Error( + "Should never come here: clientId is undefined and there are multiple providerConfigs" + ); + } + + normalisedClientId = options.config.providerConfigs[0].clientId!; + } + const providerConfig = await options.recipeImplementation.getProviderConfig({ + clientId: normalisedClientId, + userContext, + }); + + let oAuthTokensToUse: OAuthTokens = {}; + + if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { + oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo: input.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { + oAuthTokensToUse = input.oAuthTokens; + } else { + throw Error("should never come here"); + } + + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ + providerConfig, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + + const { user, recipeUserId } = await options.recipeImplementation.signIn({ + userId, + tenantId, + rawUserInfo, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + + const session = await Session.createNewSession( + options.req, + options.res, + tenantId, + recipeUserId, + undefined, + undefined, + userContext + ); + + return { + status: "OK", + user, + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfo, + }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2client/api/signin.ts b/lib/ts/recipe/oauth2client/api/signin.ts new file mode 100644 index 000000000..a4662cf27 --- /dev/null +++ b/lib/ts/recipe/oauth2client/api/signin.ts @@ -0,0 +1,84 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from "../../../error"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function signInAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.signInPOST === undefined) { + return false; + } + + const bodyParams = await options.req.getJSONBody(); + + let redirectURIInfo: + | undefined + | { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + let oAuthTokens: any; + + if (bodyParams.clientId === undefined && options.config.providerConfigs.length > 1) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: "Please provide the clientId in request body", + }); + } + + if (bodyParams.redirectURIInfo !== undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: "Please provide the redirectURI in request body", + }); + } + redirectURIInfo = bodyParams.redirectURIInfo; + } else if (bodyParams.oAuthTokens !== undefined) { + oAuthTokens = bodyParams.oAuthTokens; + } else { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", + }); + } + + let result = await apiImplementation.signInPOST({ + tenantId, + clientId: bodyParams.clientId, + redirectURIInfo, + oAuthTokens, + options, + userContext, + }); + + if (result.status === "OK") { + send200Response(options.res, { + status: result.status, + ...getBackwardsCompatibleUserInfo(options.req, result, userContext), + }); + } else { + send200Response(options.res, result); + } + return true; +} diff --git a/lib/ts/recipe/oauth2client/constants.ts b/lib/ts/recipe/oauth2client/constants.ts new file mode 100644 index 000000000..8e45f0567 --- /dev/null +++ b/lib/ts/recipe/oauth2client/constants.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/ts/recipe/oauth2client/index.ts b/lib/ts/recipe/oauth2client/index.ts new file mode 100644 index 000000000..9d175ef1c --- /dev/null +++ b/lib/ts/recipe/oauth2client/index.ts @@ -0,0 +1,81 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { getUserContext } from "../../utils"; +import { parseJWTWithoutSignatureVerification } from "../session/jwt"; +import Recipe from "./recipe"; +import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; + +export default class Wrapper { + static init = Recipe.init; + + static async exchangeAuthCodeForOAuthTokens( + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }, + clientId?: string, + userContext?: Record + ) { + let normalisedClientId = clientId; + const instance = Recipe.getInstanceOrThrowError(); + const recipeInterfaceImpl = instance.recipeInterfaceImpl; + const normalisedUserContext = getUserContext(userContext); + if (normalisedClientId === undefined) { + if (instance.config.providerConfigs.length > 1) { + throw new Error("clientId is required if there are more than one provider configs defined"); + } + + normalisedClientId = instance.config.providerConfigs[0].clientId!; + } + + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + clientId: normalisedClientId, + userContext: normalisedUserContext, + }); + return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo, + userContext: normalisedUserContext, + }); + } + + static async getUserInfo(oAuthTokens: OAuthTokens, userContext?: Record) { + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = getUserContext(userContext); + if (oAuthTokens.access_token === undefined) { + throw new Error("access_token is required to get user info"); + } + const preparseJWTInfo = parseJWTWithoutSignatureVerification(oAuthTokens.access_token!); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + clientId: preparseJWTInfo.payload.client_id, + userContext: normalisedUserContext, + }); + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ + providerConfig, + oAuthTokens, + userContext: normalisedUserContext, + }); + } +} + +export let init = Wrapper.init; + +export let exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; + +export let getUserInfo = Wrapper.getUserInfo; + +export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/ts/recipe/oauth2client/recipe.ts b/lib/ts/recipe/oauth2client/recipe.ts new file mode 100644 index 000000000..7da475c5c --- /dev/null +++ b/lib/ts/recipe/oauth2client/recipe.ts @@ -0,0 +1,137 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import RecipeModule from "../../recipeModule"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import { validateAndNormaliseUserInput } from "./utils"; +import STError from "../../error"; +import { SIGN_IN_API } from "./constants"; +import NormalisedURLPath from "../../normalisedURLPath"; +import signInAPI from "./api/signin"; +import RecipeImplementation from "./recipeImplementation"; +import APIImplementation from "./api/implementation"; +import { Querier } from "../../querier"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined; + static RECIPE_ID = "oauth2client"; + + config: TypeNormalisedInput; + + recipeInterfaceImpl: RecipeInterface; + + apiImpl: APIInterface; + + isInServerlessEnv: boolean; + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + _recipes: {} + ) { + super(recipeId, appInfo); + this.config = validateAndNormaliseUserInput(appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + + { + let builder = new OverrideableBuilder( + RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new OverrideableBuilder(APIImplementation()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); + + return Recipe.instance; + } else { + throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); + } + }; + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); + } + + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_API), + id: SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + ]; + }; + + handleAPIRequest = async ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ): Promise => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + appInfo: this.getAppInfo(), + }; + if (id === SIGN_IN_API) { + return await signInAPI(this.apiImpl, tenantId, options, userContext); + } + return false; + }; + + handleError = async (err: STError, _request: BaseRequest, _response: BaseResponse): Promise => { + throw err; + }; + + getAllCORSHeaders = (): string[] => { + return []; + }; + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + }; +} diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts new file mode 100644 index 000000000..59e90967b --- /dev/null +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -0,0 +1,185 @@ +import { + OAuthTokenResponse, + OAuthTokens, + ProviderConfigWithOIDCInfo, + RecipeInterface, + TypeNormalisedInput, + UserInfo, +} from "./types"; +import { Querier } from "../../querier"; +import RecipeUserId from "../../recipeUserId"; +import { User as UserType } from "../../types"; +import { + doGetRequest, + doPostRequest, + getOIDCDiscoveryInfo, + verifyIdTokenFromJWKSEndpointAndGetPayload, +} from "../../thirdpartyUtils"; +import { getUser } from "../.."; +import { logDebugMessage } from "../../logger"; +import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; + +export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface { + let providerConfigsWithOIDCInfo: Record = {}; + + return { + signIn: async function ({ + userId, + tenantId, + userContext, + oAuthTokens, + rawUserInfo, + }): Promise<{ + status: "OK"; + user: UserType; + recipeUserId: RecipeUserId; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + }> { + const user = await getUser(userId, userContext); + + if (user === undefined) { + throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); + } + + return { + status: "OK", + user, + recipeUserId: new RecipeUserId(userId), + oAuthTokens, + rawUserInfo, + }; + }, + getProviderConfig: async function ({ clientId }) { + if (providerConfigsWithOIDCInfo[clientId] !== undefined) { + return providerConfigsWithOIDCInfo[clientId]; + } + const providerConfig = config.providerConfigs.find( + (providerConfig) => providerConfig.clientId === clientId + )!; + const oidcInfo = await getOIDCDiscoveryInfo(providerConfig.oidcDiscoveryEndpoint); + + if (oidcInfo.authorization_endpoint === undefined) { + throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.token_endpoint === undefined) { + throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.jwks_uri === undefined) { + throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); + } + + providerConfigsWithOIDCInfo[clientId] = { + ...providerConfig, + authorizationEndpoint: oidcInfo.authorization_endpoint, + tokenEndpoint: oidcInfo.token_endpoint, + userInfoEndpoint: oidcInfo.userinfo_endpoint, + jwksURI: oidcInfo.jwks_uri, + }; + + return providerConfigsWithOIDCInfo[clientId]; + }, + exchangeAuthCodeForOAuthTokens: async function (this: RecipeInterface, { providerConfig, redirectURIInfo }) { + if (providerConfig.tokenEndpoint === undefined) { + throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); + } + const tokenAPIURL = providerConfig.tokenEndpoint; + const accessTokenAPIParams: { [key: string]: string } = { + client_id: providerConfig.clientId, + redirect_uri: redirectURIInfo.redirectURI, + code: redirectURIInfo.redirectURIQueryParams["code"], + grant_type: "authorization_code", + }; + if (providerConfig.clientSecret !== undefined) { + accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; + } + if (redirectURIInfo.pkceCodeVerifier !== undefined) { + accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + + const tokenResponse = await doPostRequest(tokenAPIURL, accessTokenAPIParams); + + if (tokenResponse.status >= 400) { + logDebugMessage( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + throw new Error( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + } + + return tokenResponse.jsonResponse as OAuthTokenResponse; + }, + getUserInfo: async function ({ providerConfig, oAuthTokens }): Promise { + let jwks: JWTVerifyGetKey | undefined; + + const accessToken = oAuthTokens["access_token"]; + const idToken = oAuthTokens["id_token"]; + + let rawUserInfo: { + fromUserInfoAPI: any; + fromIdTokenPayload: any; + } = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + + if (idToken && providerConfig.jwksURI !== undefined) { + if (jwks === undefined) { + jwks = createRemoteJWKSet(new URL(providerConfig.jwksURI)); + } + + rawUserInfo.fromIdTokenPayload = await verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, { + audience: providerConfig.clientId, + }); + } + + if (accessToken && providerConfig.userInfoEndpoint !== undefined) { + const headers: { [key: string]: string } = { + Authorization: "Bearer " + accessToken, + }; + const queryParams: { [key: string]: string } = {}; + + const userInfoFromAccessToken = await doGetRequest( + providerConfig.userInfoEndpoint, + queryParams, + headers + ); + + if (userInfoFromAccessToken.status >= 400) { + logDebugMessage( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + throw new Error( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + } + + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + } + + let userId: string | undefined = undefined; + + if (rawUserInfo.fromIdTokenPayload?.sub !== undefined) { + userId = rawUserInfo.fromIdTokenPayload["sub"]; + } else if (rawUserInfo.fromUserInfoAPI?.sub !== undefined) { + userId = rawUserInfo.fromUserInfoAPI["sub"]; + } + + if (userId === undefined) { + throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); + } + + return { + userId, + rawUserInfo, + }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2client/types.ts b/lib/ts/recipe/oauth2client/types.ts new file mode 100644 index 000000000..795e39086 --- /dev/null +++ b/lib/ts/recipe/oauth2client/types.ts @@ -0,0 +1,157 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { BaseRequest, BaseResponse } from "../../framework"; +import { NormalisedAppinfo, UserContext } from "../../types"; +import OverrideableBuilder from "supertokens-js-override"; +import { SessionContainerInterface } from "../session/types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; + +export type UserInfo = { + userId: string; + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any } }; +}; + +export type ProviderConfigInput = { + clientId: string; + clientSecret?: string; + oidcDiscoveryEndpoint: string; +}; + +export type ProviderConfigWithOIDCInfo = ProviderConfigInput & { + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; + jwksURI: string; +}; + +export type OAuthTokens = { + access_token?: string; + id_token?: string; +}; + +export type OAuthTokenResponse = { + access_token: string; + id_token?: string; + refresh_token?: string; + expires_in: number; + scope?: string; + token_type: string; +}; + +export type TypeInput = { + providerConfigs: ProviderConfigInput[]; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type TypeNormalisedInput = { + providerConfigs: ProviderConfigInput[]; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type RecipeInterface = { + getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; + + signIn(input: { + userId: string; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + tenantId: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + }>; + exchangeAuthCodeForOAuthTokens(input: { + providerConfig: ProviderConfigWithOIDCInfo; + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }; + userContext: UserContext; + }): Promise; + getUserInfo(input: { + providerConfig: ProviderConfigWithOIDCInfo; + oAuthTokens: OAuthTokens; + userContext: UserContext; + }): Promise; +}; + +export type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; + appInfo: NormalisedAppinfo; +}; + +export type APIInterface = { + signInPOST: ( + input: { + tenantId: string; + clientId?: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + } + | { + oAuthTokens: { [key: string]: any }; + } + ) + ) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + oAuthTokens: { [key: string]: any }; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | GeneralErrorResponse + >; +}; diff --git a/lib/ts/recipe/oauth2client/utils.ts b/lib/ts/recipe/oauth2client/utils.ts new file mode 100644 index 000000000..54c990144 --- /dev/null +++ b/lib/ts/recipe/oauth2client/utils.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; + +export function validateAndNormaliseUserInput(_appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { + if (config === undefined || config.providerConfigs === undefined) { + throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); + } + + if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { + throw new Error("Please pass clientId for all providerConfigs."); + } + + if (!config.providerConfigs.every((providerConfig) => providerConfig.clientId.startsWith("stcl_"))) { + throw new Error( + `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the ThirdParty recipe.` + ); + } + + if (config.providerConfigs.some((providerConfig) => providerConfig.oidcDiscoveryEndpoint === undefined)) { + throw new Error("Please pass oidcDiscoveryEndpoint for all providerConfigs."); + } + + let override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + }; + + return { + providerConfigs: config.providerConfigs, + override, + }; +} diff --git a/lib/ts/recipe/oauth2provider/OAuth2Client.ts b/lib/ts/recipe/oauth2provider/OAuth2Client.ts new file mode 100644 index 000000000..92546443b --- /dev/null +++ b/lib/ts/recipe/oauth2provider/OAuth2Client.ts @@ -0,0 +1,243 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { transformObjectKeys } from "../../utils"; +import { OAuth2ClientOptions } from "./types"; + +export class OAuth2Client { + /** + * OAuth 2.0 Client ID + * The ID is immutable. If no ID is provided, a UUID4 will be generated. + */ + clientId: string; + + /** + * OAuth 2.0 Client Secret + * The secret will be included in the create request as cleartext, and then + * never again. The secret is kept in hashed format and is not recoverable once lost. + */ + clientSecret?: string; + + /** + * OAuth 2.0 Client Name + * The human-readable name of the client to be presented to the end-user during authorization. + */ + clientName: string; + + /** + * OAuth 2.0 Client Scope + * Scope is a string containing a space-separated list of scope values that the client + * can use when requesting access tokens. + */ + scope: string; + + /** + * Array of redirect URIs + */ + redirectUris: string[] | null; + + /** + * Array of post logout redirect URIs + * + * This field holds a list of whitelisted `post_logout_redirect_uri`s used to redirect the user after + * logout via the `end_session_endpoint`. If a non-whitelisted URI is provided, the logout request is rejected. + * + * By default, this field is absent in the OAuth2Client. If provided, it must be a non-empty array of strings, + * with each URI’s domain, port, and scheme matching at least one registered redirect URI. + */ + postLogoutRedirectUris?: string[]; + + /** + * Authorization Code Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantAccessTokenLifespan: string | null; + + /** + * Authorization Code Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantIdTokenLifespan: string | null; + + /** + * Authorization Code Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantRefreshTokenLifespan: string | null; + + /** + * Client Credentials Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + clientCredentialsGrantAccessTokenLifespan: string | null; + + /** + * Implicit Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantAccessTokenLifespan: string | null; + + /** + * Implicit Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantIdTokenLifespan: string | null; + + /** + * Refresh Token Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantAccessTokenLifespan: string | null; + + /** + * Refresh Token Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantIdTokenLifespan: string | null; + + /** + * Refresh Token Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantRefreshTokenLifespan: string | null; + + /** + * OAuth 2.0 Token Endpoint Authentication Method + * Requested Client Authentication method for the Token Endpoint. + */ + tokenEndpointAuthMethod: string; + + /** + * OAuth 2.0 Client URI + * ClientURI is a URL string of a web page providing information about the client. + */ + clientUri: string; + + /** + * Array of audiences + */ + audience: string[]; + + /** + * Array of grant types + */ + grantTypes: string[] | null; + + /** + * Array of response types + */ + responseTypes: string[] | null; + + /** + * OAuth 2.0 Client Logo URI + * A URL string referencing the client's logo. + */ + logoUri: string; + + /** + * OAuth 2.0 Client Policy URI + * PolicyURI is a URL string that points to a human-readable privacy policy document + * that describes how the deployment organization collects, uses, + * retains, and discloses personal data. + */ + policyUri: string; + + /** + * OAuth 2.0 Client Terms of Service URI + * A URL string pointing to a human-readable terms of service + * document for the client that describes a contractual relationship + * between the end-user and the client that the end-user accepts when + * authorizing the client. + */ + tosUri: string; + + /** + * OAuth 2.0 Client Creation Date + * CreatedAt returns the timestamp of the client's creation. + */ + createdAt: string; + + /** + * OAuth 2.0 Client Last Update Date + * UpdatedAt returns the timestamp of the last update. + */ + updatedAt: string; + + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + metadata: Record = {}; + + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris = null, + postLogoutRedirectUris, + authorizationCodeGrantAccessTokenLifespan = null, + authorizationCodeGrantIdTokenLifespan = null, + authorizationCodeGrantRefreshTokenLifespan = null, + clientCredentialsGrantAccessTokenLifespan = null, + implicitGrantAccessTokenLifespan = null, + implicitGrantIdTokenLifespan = null, + refreshTokenGrantAccessTokenLifespan = null, + refreshTokenGrantIdTokenLifespan = null, + refreshTokenGrantRefreshTokenLifespan = null, + tokenEndpointAuthMethod, + clientUri = "", + audience = [], + grantTypes = null, + responseTypes = null, + logoUri = "", + policyUri = "", + tosUri = "", + createdAt, + updatedAt, + metadata = {}, + }: OAuth2ClientOptions) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.clientName = clientName; + this.scope = scope; + this.redirectUris = redirectUris; + this.postLogoutRedirectUris = postLogoutRedirectUris; + this.authorizationCodeGrantAccessTokenLifespan = authorizationCodeGrantAccessTokenLifespan; + this.authorizationCodeGrantIdTokenLifespan = authorizationCodeGrantIdTokenLifespan; + this.authorizationCodeGrantRefreshTokenLifespan = authorizationCodeGrantRefreshTokenLifespan; + this.clientCredentialsGrantAccessTokenLifespan = clientCredentialsGrantAccessTokenLifespan; + this.implicitGrantAccessTokenLifespan = implicitGrantAccessTokenLifespan; + this.implicitGrantIdTokenLifespan = implicitGrantIdTokenLifespan; + this.refreshTokenGrantAccessTokenLifespan = refreshTokenGrantAccessTokenLifespan; + this.refreshTokenGrantIdTokenLifespan = refreshTokenGrantIdTokenLifespan; + this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; + this.clientUri = clientUri; + this.audience = audience; + this.grantTypes = grantTypes; + this.responseTypes = responseTypes; + this.logoUri = logoUri; + this.policyUri = policyUri; + this.tosUri = tosUri; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.metadata = metadata; + } + + static fromAPIResponse(response: any): OAuth2Client { + return new OAuth2Client(transformObjectKeys(response, "camelCase")); + } +} diff --git a/lib/ts/recipe/oauth2provider/api/auth.ts b/lib/ts/recipe/oauth2provider/api/auth.ts new file mode 100644 index 000000000..349e4e187 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/auth.ts @@ -0,0 +1,85 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import setCookieParser from "set-cookie-parser"; +import Session from "../../session"; +import SessionError from "../../../recipe/session/error"; + +export default async function authGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.authGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + let session, shouldTryRefresh; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + session = undefined; + if (SessionError.isErrorFromSuperTokens(error) && error.type === SessionError.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + // This should generally not happen, but we can handle this as if the session is not present, + // because then we redirect to the frontend, which should handle the validation error + shouldTryRefresh = false; + } + } + + let response = await apiImplementation.authGET({ + options, + params: Object.fromEntries(params.entries()), + cookie: options.req.getHeaderValue("cookie"), + session, + shouldTryRefresh, + userContext, + }); + + if ("redirectTo" in response) { + if (response.cookies) { + const cookieStr = setCookieParser.splitCookiesString(response.cookies); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); + } + } + options.res.original.redirect(response.redirectTo); + } else if ("statusCode" in response) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/endSession.ts b/lib/ts/recipe/oauth2provider/api/endSession.ts new file mode 100644 index 000000000..e298b8271 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/endSession.ts @@ -0,0 +1,100 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import Session from "../../session"; +import SuperTokensError from "../../../error"; +import SessionError from "../../../recipe/session/error"; + +export async function endSessionGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.endSessionGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + + return endSessionCommon( + Object.fromEntries(params.entries()), + apiImplementation.endSessionGET, + options, + userContext + ); +} + +export async function endSessionPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.endSessionPOST === undefined) { + return false; + } + const params = await options.req.getBodyAsJSONOrFormData(); + + return endSessionCommon(params, apiImplementation.endSessionPOST, options, userContext); +} + +async function endSessionCommon( + params: Record, + apiImplementation: APIInterface["endSessionGET"] | APIInterface["endSessionPOST"], + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation === undefined) { + return false; + } + + let session, shouldTryRefresh; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (SuperTokensError.isErrorFromSuperTokens(error) && error.type === SessionError.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + + let response = await apiImplementation({ + options, + params, + session, + shouldTryRefresh, + userContext, + }); + + if ("redirectTo" in response) { + options.res.original.redirect(response.redirectTo); + } else if ("error" in response) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts new file mode 100644 index 000000000..7a13bee92 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -0,0 +1,201 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface } from "../types"; +import { handleLoginInternalRedirects, handleLogoutInternalRedirects, loginGET } from "./utils"; + +export default function getAPIImplementation(): APIInterface { + return { + loginGET: async ({ loginChallenge, options, session, shouldTryRefresh, userContext }) => { + const response = await loginGET({ + recipeImplementation: options.recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + isDirectCall: true, + userContext, + }); + + if ("error" in response) { + return response; + } + + const respAfterInternalRedirects = await handleLoginInternalRedirects({ + response, + cookie: options.req.getHeaderValue("cookie"), + recipeImplementation: options.recipeImplementation, + session, + shouldTryRefresh, + userContext, + }); + + if ("error" in respAfterInternalRedirects) { + return respAfterInternalRedirects; + } + + return { + frontendRedirectTo: respAfterInternalRedirects.redirectTo, + cookies: respAfterInternalRedirects.cookies, + }; + }, + + authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.authorization({ + params, + cookies: cookie, + session, + userContext, + }); + + if ("error" in response) { + return response; + } + + return handleLoginInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + cookie, + session, + shouldTryRefresh, + userContext, + }); + }, + tokenPOST: async (input) => { + return input.options.recipeImplementation.tokenExchange({ + authorizationHeader: input.authorizationHeader, + body: input.body, + userContext: input.userContext, + }); + }, + loginInfoGET: async ({ loginChallenge, options, userContext }) => { + const loginRes = await options.recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + + if (loginRes.status === "ERROR") { + return loginRes; + } + const { client } = loginRes; + + return { + status: "OK", + info: { + clientId: client.clientId, + clientName: client.clientName, + tosUri: client.tosUri, + policyUri: client.policyUri, + logoUri: client.logoUri, + clientUri: client.clientUri, + metadata: client.metadata, + }, + }; + }, + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { + return options.recipeImplementation.buildUserInfo({ + user, + accessTokenPayload, + scopes, + tenantId, + userContext, + }); + }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else if ("clientId" in input && input.clientId !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } else { + throw new Error(`Either of 'authorizationHeader' or 'clientId' must be provided`); + } + }, + introspectTokenPOST: async (input) => { + return input.options.recipeImplementation.introspectToken({ + token: input.token, + scopes: input.scopes, + userContext: input.userContext, + }); + }, + endSessionGET: async ({ options, params, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.endSession({ + params, + session, + shouldTryRefresh, + userContext, + }); + + if ("error" in response) { + return response; + } + + return handleLogoutInternalRedirects({ + response, + session, + recipeImplementation: options.recipeImplementation, + userContext, + }); + }, + endSessionPOST: async ({ options, params, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.endSession({ + params, + session, + shouldTryRefresh, + userContext, + }); + + if ("error" in response) { + return response; + } + + return handleLogoutInternalRedirects({ + response, + session, + recipeImplementation: options.recipeImplementation, + userContext, + }); + }, + logoutPOST: async ({ logoutChallenge, options, session, userContext }) => { + if (session != undefined) { + await session.revokeSession(userContext); + } + + const response = await options.recipeImplementation.acceptLogoutRequest({ + challenge: logoutChallenge, + userContext, + }); + + const res = await handleLogoutInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + userContext, + }); + + if ("error" in res) { + return res; + } + + return { status: "OK", frontendRedirectTo: res.redirectTo }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2provider/api/introspectToken.ts b/lib/ts/recipe/oauth2provider/api/introspectToken.ts new file mode 100644 index 000000000..47b4be69c --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/introspectToken.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function introspectTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.introspectTokenPOST === undefined) { + return false; + } + + const body = await options.req.getBodyAsJSONOrFormData(); + + if (body.token === undefined) { + sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + + const scopes: string[] = body.scope ? body.scope.split(" ") : []; + + let response = await apiImplementation.introspectTokenPOST({ + options, + token: body.token, + scopes, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/login.ts b/lib/ts/recipe/oauth2provider/api/login.ts new file mode 100644 index 000000000..e399b2877 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/login.ts @@ -0,0 +1,93 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import setCookieParser from "set-cookie-parser"; +import { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import Session from "../../session"; +import { UserContext } from "../../../types"; +import SuperTokensError from "../../../error"; +import SessionError from "../../../recipe/session/error"; + +export default async function login( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.loginGET === undefined) { + return false; + } + + let session, shouldTryRefresh; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (SuperTokensError.isErrorFromSuperTokens(error) && error.type === SessionError.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + + const loginChallenge = + options.req.getKeyValueFromQuery("login_challenge") ?? options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new SuperTokensError({ + type: SuperTokensError.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginGET({ + options, + loginChallenge, + session, + shouldTryRefresh, + userContext, + }); + + if ("frontendRedirectTo" in response) { + if (response.cookies) { + const cookieStr = setCookieParser.splitCookiesString(response.cookies); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); + } + } + send200Response(options.res, { + frontendRedirectTo: response.frontendRedirectTo, + }); + } else if ("statusCode" in response) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/loginInfo.ts b/lib/ts/recipe/oauth2provider/api/loginInfo.ts new file mode 100644 index 000000000..2c13ddad0 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/loginInfo.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import SuperTokensError from "../../../error"; + +export default async function loginInfoGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.loginInfoGET === undefined) { + return false; + } + + const loginChallenge = + options.req.getKeyValueFromQuery("login_challenge") ?? options.req.getKeyValueFromQuery("loginChallenge"); + + if (loginChallenge === undefined) { + throw new SuperTokensError({ + type: SuperTokensError.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + + let response = await apiImplementation.loginInfoGET({ + options, + loginChallenge, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/logout.ts b/lib/ts/recipe/oauth2provider/api/logout.ts new file mode 100644 index 000000000..55c1c83ce --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/logout.ts @@ -0,0 +1,66 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import Session from "../../session"; +import { UserContext } from "../../../types"; +import SuperTokensError from "../../../error"; + +export async function logoutPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.logoutPOST === undefined) { + return false; + } + + let session; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + } catch { + session = undefined; + } + + const body = await options.req.getBodyAsJSONOrFormData(); + + if (body.logoutChallenge === undefined) { + throw new SuperTokensError({ + type: SuperTokensError.BAD_INPUT_ERROR, + message: "Missing body param: logoutChallenge", + }); + } + + let response = await apiImplementation.logoutPOST({ + options, + logoutChallenge: body.logoutChallenge, + session, + userContext, + }); + + if ("status" in response && response.status === "OK") { + send200Response(options.res, response); + } else if ("statusCode" in response) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/revokeToken.ts b/lib/ts/recipe/oauth2provider/api/revokeToken.ts new file mode 100644 index 000000000..46bb65692 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/revokeToken.ts @@ -0,0 +1,65 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function revokeTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + + const body = await options.req.getBodyAsJSONOrFormData(); + + if (body.token === undefined) { + sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + + const authorizationHeader = options.req.getHeaderValue("authorization"); + + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + + if ("statusCode" in response && response.statusCode !== 200) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/token.ts b/lib/ts/recipe/oauth2provider/api/token.ts new file mode 100644 index 000000000..cd546567c --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/token.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function tokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.tokenPOST === undefined) { + return false; + } + + const authorizationHeader = options.req.getHeaderValue("authorization"); + + let response = await apiImplementation.tokenPOST({ + authorizationHeader, + options, + body: await options.req.getBodyAsJSONOrFormData(), + userContext, + }); + + if ("error" in response) { + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/userInfo.ts b/lib/ts/recipe/oauth2provider/api/userInfo.ts new file mode 100644 index 000000000..3e661068d --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/userInfo.ts @@ -0,0 +1,92 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OAuth2ProviderRecipe from "../recipe"; +import { send200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { JSONObject, UserContext } from "../../../types"; +import { getUser } from "../../.."; + +export default async function userInfoGET( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.userInfoGET === undefined) { + return false; + } + + const authHeader = options.req.getHeaderValue("authorization"); + + if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { + sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + return true; + } + + const accessToken = authHeader.replace(/^Bearer /, "").trim(); + + let accessTokenPayload: JSONObject; + + try { + const { + payload, + } = await OAuth2ProviderRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + userContext, + }); + accessTokenPayload = payload; + } catch (error) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 401); + return true; + } + + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + !Array.isArray(accessTokenPayload.scp) + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 401); + return true; + } + + const userId = accessTokenPayload.sub; + + const user = await getUser(userId, userContext); + + if (user === undefined) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Couldn't find any user associated with the access token", 401); + return true; + } + + const response = await apiImplementation.userInfoGET({ + accessTokenPayload, + user, + tenantId, + scopes: accessTokenPayload.scp as string[], + options, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/utils.ts b/lib/ts/recipe/oauth2provider/api/utils.ts new file mode 100644 index 000000000..bebdced8a --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/utils.ts @@ -0,0 +1,324 @@ +import SuperTokens from "../../../supertokens"; +import { UserContext } from "../../../types"; +import { DEFAULT_TENANT_ID } from "../../multitenancy/constants"; +import { getSessionInformation } from "../../session"; +import { SessionContainerInterface } from "../../session/types"; +import { AUTH_PATH, LOGIN_PATH, END_SESSION_PATH } from "../constants"; +import { ErrorOAuth2, RecipeInterface } from "../types"; +import setCookieParser from "set-cookie-parser"; + +// API implementation for the loginGET function. +// Extracted for use in both apiImplementation and handleInternalRedirects. +export async function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + cookies, + isDirectCall, + userContext, +}: { + recipeImplementation: RecipeInterface; + loginChallenge: string; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookies?: string; + userContext: UserContext; + isDirectCall: boolean; +}) { + const loginRequest = await recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + + if (loginRequest.status === "ERROR") { + return loginRequest; + } + + const sessionInfo = session !== undefined ? await getSessionInformation(session?.getHandle()) : undefined; + if (!sessionInfo) { + session = undefined; + } + + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = incomingAuthUrlQueryParams.get("prompt") ?? incomingAuthUrlQueryParams.get("st_prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + + if (maxAgeParam !== null) { + try { + const maxAgeParsed = Number.parseInt(maxAgeParam); + + if (Number.isNaN(maxAgeParsed)) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + + if (maxAgeParsed < 0) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age cannot be negative", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + } catch { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + } + const tenantIdParam = incomingAuthUrlQueryParams.get("tenant_id"); + if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (["", null].includes(tenantIdParam) || session.getTenantId() === tenantIdParam) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo!.timeCreated) + ) { + const accept = await recipeImplementation.acceptLoginRequest({ + challenge: loginChallenge, + subject: session.getUserId(), + identityProviderSessionId: session.getHandle(), + userContext, + }); + return { status: "REDIRECT", redirectTo: accept.redirectTo, cookies: cookies }; + } + + if (shouldTryRefresh && promptParam !== "login") { + return { + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "try-refresh", + loginChallenge, + userContext, + }), + cookies: cookies, + }; + } + if (promptParam === "none") { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "ERROR", + error: "login_required", + errorDescription: + "The Authorization Server requires End-User authentication. Prompt 'none' was requested, but no existing or expired login session was found.", + }, + userContext, + }); + return { status: "REDIRECT", redirectTo: reject.redirectTo, cookies }; + } + + return { + status: "REDIRECT", + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "login", + loginChallenge, + forceFreshAuth: session !== undefined || promptParam === "login", + tenantId: tenantIdParam ?? DEFAULT_TENANT_ID, + hint: loginRequest.oidcContext?.login_hint, + userContext, + }), + cookies, + }; +} + +function getMergedCookies({ origCookies = "", newCookies }: { origCookies?: string; newCookies?: string }): string { + if (!newCookies) { + return origCookies; + } + + const cookieMap = origCookies.split(";").reduce((acc, curr) => { + const [name, value] = curr.split("="); + return { ...acc, [name.trim()]: value }; + }, {} as Record); + + const setCookies = setCookieParser.parse(setCookieParser.splitCookiesString(newCookies)); + + for (const { name, value, expires } of setCookies) { + if (expires && new Date(expires) < new Date()) { + delete cookieMap[name]; + } else { + cookieMap[name] = value; + } + } + + return Object.entries(cookieMap) + .map(([key, value]) => `${key}=${value}`) + .join(";"); +} + +function mergeSetCookieHeaders(setCookie1?: string, setCookie2?: string): string { + if (!setCookie1) { + return setCookie2 || ""; + } + if (!setCookie2 || setCookie1 === setCookie2) { + return setCookie1; + } + return `${setCookie1}, ${setCookie2}`; +} + +function isLoginInternalRedirect(redirectTo: string): boolean { + const { apiDomain, apiBasePath } = SuperTokens.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + + return [LOGIN_PATH, AUTH_PATH].some((path) => redirectTo.startsWith(`${basePath}${path}`)); +} + +function isLogoutInternalRedirect(redirectTo: string): boolean { + const { apiDomain, apiBasePath } = SuperTokens.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + return redirectTo.startsWith(`${basePath}${END_SESSION_PATH}`); +} + +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/login and /oauth/auth endpoints in the login flow. +export async function handleLoginInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie = "", + userContext, +}: { + response: { redirectTo: string; cookies?: string }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookie?: string; + userContext: UserContext; +}): Promise<{ redirectTo: string; cookies?: string } | ErrorOAuth2> { + if (!isLoginInternalRedirect(response.redirectTo)) { + return response; + } + + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + + while (redirectCount < maxRedirects && isLoginInternalRedirect(response.redirectTo)) { + cookie = getMergedCookies({ origCookies: cookie, newCookies: response.cookies }); + + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + + if (response.redirectTo.includes(LOGIN_PATH)) { + const loginChallenge = params.get("login_challenge") ?? params.get("loginChallenge"); + if (!loginChallenge) { + throw new Error(`Expected loginChallenge in ${response.redirectTo}`); + } + + const loginRes = await loginGET({ + recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + cookies: response.cookies, + isDirectCall: false, + userContext, + }); + + if ("error" in loginRes) { + return loginRes; + } + + response = { + redirectTo: loginRes.redirectTo, + cookies: mergeSetCookieHeaders(loginRes.cookies, response.cookies), + }; + } else if (response.redirectTo.includes(AUTH_PATH)) { + const authRes = await recipeImplementation.authorization({ + params: Object.fromEntries(params.entries()), + cookies: cookie, + session, + userContext, + }); + + if ("error" in authRes) { + return authRes; + } + + response = { + redirectTo: authRes.redirectTo, + cookies: mergeSetCookieHeaders(authRes.cookies, response.cookies), + }; + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + + redirectCount++; + } + return response; +} + +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/end_session endpoint in the logout flow. +export async function handleLogoutInternalRedirects({ + response, + recipeImplementation, + session, + userContext, +}: { + response: { redirectTo: string }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + userContext: UserContext; +}): Promise<{ redirectTo: string } | ErrorOAuth2> { + if (!isLogoutInternalRedirect(response.redirectTo)) { + return response; + } + + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + + while (redirectCount < maxRedirects && isLogoutInternalRedirect(response.redirectTo)) { + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + + if (response.redirectTo.includes(END_SESSION_PATH)) { + const endSessionRes = await recipeImplementation.endSession({ + params: Object.fromEntries(params.entries()), + session, + // We internally redirect to the `end_session_endpoint` at the end of the logout flow. + // This involves calling Hydra with the `logout_verifier`, after which Hydra redirects to the `post_logout_redirect_uri`. + // We set `shouldTryRefresh` to `false` since the SuperTokens session isn't needed to handle this request. + shouldTryRefresh: false, + userContext, + }); + if ("error" in endSessionRes) { + return endSessionRes; + } + response = endSessionRes; + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + + redirectCount++; + } + return response; +} diff --git a/lib/ts/recipe/oauth2provider/constants.ts b/lib/ts/recipe/oauth2provider/constants.ts new file mode 100644 index 000000000..f6c696b66 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/constants.ts @@ -0,0 +1,26 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const OAUTH2_BASE_PATH = "/oauth/"; + +export const LOGIN_PATH = "/oauth/login"; +export const AUTH_PATH = "/oauth/auth"; +export const TOKEN_PATH = "/oauth/token"; +export const LOGIN_INFO_PATH = "/oauth/login/info"; +export const USER_INFO_PATH = "/oauth/userinfo"; +export const REVOKE_TOKEN_PATH = "/oauth/revoke"; +export const INTROSPECT_TOKEN_PATH = "/oauth/introspect"; +export const END_SESSION_PATH = "/oauth/end_session"; +export const LOGOUT_PATH = "/oauth/logout"; diff --git a/lib/ts/recipe/oauth2provider/index.ts b/lib/ts/recipe/oauth2provider/index.ts new file mode 100644 index 000000000..95ad3ce8b --- /dev/null +++ b/lib/ts/recipe/oauth2provider/index.ts @@ -0,0 +1,183 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { getUserContext } from "../../utils"; +import Recipe from "./recipe"; +import { + APIInterface, + RecipeInterface, + APIOptions, + CreateOAuth2ClientInput, + UpdateOAuth2ClientInput, + DeleteOAuth2ClientInput, + GetOAuth2ClientsInput, +} from "./types"; + +export default class Wrapper { + static init = Recipe.init; + + static async getOAuth2Client(clientId: string, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client({ + clientId, + userContext: getUserContext(userContext), + }); + } + static async getOAuth2Clients(input: GetOAuth2ClientsInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Clients({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async createOAuth2Client(input: CreateOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async updateOAuth2Client(input: UpdateOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async deleteOAuth2Client(input: DeleteOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.deleteOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + + static validateOAuth2AccessToken( + token: string, + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }, + checkDatabase?: boolean, + userContext?: Record + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token, + requirements, + checkDatabase, + userContext: getUserContext(userContext), + }); + } + + static createTokenForClientCredentials( + clientId: string, + clientSecret: string, + scope?: string[], + audience?: string, + userContext?: Record + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.tokenExchange({ + body: { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + scope: scope?.join(" "), + audience: audience, + }, + userContext: getUserContext(userContext), + }); + } + + static async revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ) { + let authorizationHeader: string | undefined = undefined; + + const normalisedUserContext = getUserContext(userContext); + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId, userContext: normalisedUserContext }); + + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + + const { tokenEndpointAuthMethod } = res.client; + + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } + + static async revokeTokensByClientId(clientId: string, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeTokensByClientId({ + clientId, + userContext: getUserContext(userContext), + }); + } + + static async revokeTokensBySessionHandle(sessionHandle: string, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeTokensBySessionHandle({ + sessionHandle, + userContext: getUserContext(userContext), + }); + } + + static validateOAuth2RefreshToken(token: string, scopes?: string[], userContext?: Record) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.introspectToken({ + token, + scopes, + userContext: getUserContext(userContext), + }); + } +} + +export let init = Wrapper.init; + +export let getOAuth2Client = Wrapper.getOAuth2Client; +export let getOAuth2Clients = Wrapper.getOAuth2Clients; + +export let createOAuth2Client = Wrapper.createOAuth2Client; + +export let updateOAuth2Client = Wrapper.updateOAuth2Client; + +export let deleteOAuth2Client = Wrapper.deleteOAuth2Client; + +export let validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; +export let validateOAuth2RefreshToken = Wrapper.validateOAuth2RefreshToken; + +export let createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; + +export let revokeToken = Wrapper.revokeToken; +export let revokeTokensByClientId = Wrapper.revokeTokensByClientId; +export let revokeTokensBySessionHandle = Wrapper.revokeTokensBySessionHandle; + +export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts new file mode 100644 index 000000000..bdd4e0e64 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -0,0 +1,337 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import SuperTokensError from "../../error"; +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import NormalisedURLPath from "../../normalisedURLPath"; +import { Querier } from "../../querier"; +import RecipeModule from "../../recipeModule"; +import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; +import authGET from "./api/auth"; +import APIImplementation from "./api/implementation"; +import loginAPI from "./api/login"; +import tokenPOST from "./api/token"; +import loginInfoGET from "./api/loginInfo"; +import { + AUTH_PATH, + INTROSPECT_TOKEN_PATH, + LOGIN_INFO_PATH, + LOGIN_PATH, + LOGOUT_PATH, + END_SESSION_PATH, + REVOKE_TOKEN_PATH, + TOKEN_PATH, + USER_INFO_PATH, +} from "./constants"; +import RecipeImplementation from "./recipeImplementation"; +import { + APIInterface, + PayloadBuilderFunction, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + UserInfo, + UserInfoBuilderFunction, +} from "./types"; +import { validateAndNormaliseUserInput } from "./utils"; +import OverrideableBuilder from "supertokens-js-override"; +import { User } from "../../user"; +import userInfoGET from "./api/userInfo"; +import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; +import revokeTokenPOST from "./api/revokeToken"; +import introspectTokenPOST from "./api/introspectToken"; +import { endSessionGET, endSessionPOST } from "./api/endSession"; +import { logoutPOST } from "./api/logout"; + +export default class Recipe extends RecipeModule { + static RECIPE_ID = "oauth2provider"; + private static instance: Recipe | undefined = undefined; + private accessTokenBuilders: PayloadBuilderFunction[] = []; + private idTokenBuilders: PayloadBuilderFunction[] = []; + private userInfoBuilders: UserInfoBuilderFunction[] = []; + + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo); + this.config = validateAndNormaliseUserInput(this, appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + + { + let builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(recipeId), + this.config, + appInfo, + this.getDefaultAccessTokenPayload.bind(this), + this.getDefaultIdTokenPayload.bind(this), + this.getDefaultUserInfoPayload.bind(this) + ) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new OverrideableBuilder(APIImplementation()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + + /* Init functions */ + + static getInstance(): Recipe | undefined { + return Recipe.instance; + } + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the Jwt.init function?"); + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + return Recipe.instance; + } else { + throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs."); + } + }; + } + + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + resetCombinedJWKS(); + Recipe.instance = undefined; + } + + addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn: UserInfoBuilderFunction) => { + this.userInfoBuilders.push(userInfoBuilderFn); + }; + addAccessTokenBuilderFromOtherRecipe = (accessTokenBuilders: PayloadBuilderFunction) => { + this.accessTokenBuilders.push(accessTokenBuilders); + }; + addIdTokenBuilderFromOtherRecipe = (idTokenBuilder: PayloadBuilderFunction) => { + this.idTokenBuilders.push(idTokenBuilder); + }; + + /* RecipeModule functions */ + + getAPIsHandled(): APIHandled[] { + return [ + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(LOGIN_PATH), + id: LOGIN_PATH, + disabled: this.apiImpl.loginGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(TOKEN_PATH), + id: TOKEN_PATH, + disabled: this.apiImpl.tokenPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(AUTH_PATH), + id: AUTH_PATH, + disabled: this.apiImpl.authGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(LOGIN_INFO_PATH), + id: LOGIN_INFO_PATH, + disabled: this.apiImpl.loginInfoGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(USER_INFO_PATH), + id: USER_INFO_PATH, + disabled: this.apiImpl.userInfoGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(REVOKE_TOKEN_PATH), + id: REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(INTROSPECT_TOKEN_PATH), + id: INTROSPECT_TOKEN_PATH, + disabled: this.apiImpl.introspectTokenPOST === undefined, + }, + + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(END_SESSION_PATH), + id: END_SESSION_PATH, + disabled: this.apiImpl.endSessionGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(END_SESSION_PATH), + id: END_SESSION_PATH, + disabled: this.apiImpl.endSessionPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(LOGOUT_PATH), + id: LOGOUT_PATH, + disabled: this.apiImpl.logoutPOST === undefined, + }, + ]; + } + + handleAPIRequest = async ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + method: HTTPMethod, + userContext: UserContext + ): Promise => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + + if (id === LOGIN_PATH) { + return loginAPI(this.apiImpl, options, userContext); + } + if (id === TOKEN_PATH) { + return tokenPOST(this.apiImpl, options, userContext); + } + if (id === AUTH_PATH) { + return authGET(this.apiImpl, options, userContext); + } + if (id === LOGIN_INFO_PATH) { + return loginInfoGET(this.apiImpl, options, userContext); + } + if (id === USER_INFO_PATH) { + return userInfoGET(this.apiImpl, tenantId, options, userContext); + } + if (id === REVOKE_TOKEN_PATH) { + return revokeTokenPOST(this.apiImpl, options, userContext); + } + if (id === INTROSPECT_TOKEN_PATH) { + return introspectTokenPOST(this.apiImpl, options, userContext); + } + if (id === END_SESSION_PATH && method === "get") { + return endSessionGET(this.apiImpl, options, userContext); + } + if (id === END_SESSION_PATH && method === "post") { + return endSessionPOST(this.apiImpl, options, userContext); + } + if (id === LOGOUT_PATH && method === "post") { + return logoutPOST(this.apiImpl, options, userContext); + } + throw new Error("Should never come here: handleAPIRequest called with unknown id"); + }; + + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise { + throw error; + } + + getAllCORSHeaders(): string[] { + return []; + } + + isErrorFromThisRecipe(err: any): err is error { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + + async getDefaultAccessTokenPayload(user: User, scopes: string[], sessionHandle: string, userContext: UserContext) { + let payload: JSONObject = {}; + + for (const fn of this.accessTokenBuilders) { + payload = { + ...payload, + ...(await fn(user, scopes, sessionHandle, userContext)), + }; + } + + return payload; + } + async getDefaultIdTokenPayload(user: User, scopes: string[], sessionHandle: string, userContext: UserContext) { + let payload: JSONObject = {}; + if (scopes.includes("email")) { + payload.email = user?.emails[0]; + payload.email_verified = user.loginMethods.some((lm) => lm.hasSameEmailAs(user?.emails[0]) && lm.verified); + payload.emails = user.emails; + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user?.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => lm.hasSamePhoneNumberAs(user?.phoneNumbers[0]) && lm.verified + ); + payload.phoneNumbers = user.phoneNumbers; + } + + for (const fn of this.idTokenBuilders) { + payload = { + ...payload, + ...(await fn(user, scopes, sessionHandle, userContext)), + }; + } + + return payload; + } + + async getDefaultUserInfoPayload( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext + ) { + let payload: JSONObject = { + sub: accessTokenPayload.sub, + }; + if (scopes.includes("email")) { + // TODO: try and get the email based on the user id of the entire user object + payload.email = user?.emails[0]; + payload.email_verified = user.loginMethods.some((lm) => lm.hasSameEmailAs(user?.emails[0]) && lm.verified); + payload.emails = user.emails; + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user?.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => lm.hasSamePhoneNumberAs(user?.phoneNumbers[0]) && lm.verified + ); + payload.phoneNumbers = user.phoneNumbers; + } + + for (const fn of this.userInfoBuilders) { + payload = { + ...payload, + ...(await fn(user, accessTokenPayload, scopes, tenantId, userContext)), + }; + } + + return payload as UserInfo; + } +} diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts new file mode 100644 index 000000000..2e7a62e43 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -0,0 +1,866 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import * as jose from "jose"; +import NormalisedURLPath from "../../normalisedURLPath"; +import { Querier } from "../../querier"; +import { JSONObject, NormalisedAppinfo } from "../../types"; +import { + RecipeInterface, + TypeNormalisedInput, + ConsentRequest, + PayloadBuilderFunction, + UserInfoBuilderFunction, +} from "./types"; +import { OAuth2Client } from "./OAuth2Client"; +import { getUser } from "../.."; +import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; +import SessionRecipe from "../session/recipe"; +import OpenIdRecipe from "../openid/recipe"; +import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; + +function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); +} + +function copyAndCleanRequestBodyInput(input: any): any { + let result = { + ...input, + }; + delete result.userContext; + delete result.tenantId; + delete result.session; + + return result; +} + +export default function getRecipeInterface( + querier: Querier, + _config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, + getDefaultAccessTokenPayload: PayloadBuilderFunction, + getDefaultIdTokenPayload: PayloadBuilderFunction, + getDefaultUserInfoPayload: UserInfoBuilderFunction +): RecipeInterface { + return { + getLoginRequest: async function (this: RecipeInterface, input) { + const resp = await querier.sendGetRequest( + new NormalisedURLPath("/recipe/oauth/auth/requests/login"), + { loginChallenge: input.challenge }, + input.userContext + ); + if (resp.status !== "OK") { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + return { + status: "OK", + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/accept`), + { + acr: input.acr, + amr: input.amr, + context: input.context, + extendSessionLifespan: input.extendSessionLifespan, + identityProviderSessionId: input.identityProviderSessionId, + subject: input.subject, + }, + { + loginChallenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/reject`), + { + error: input.error.error, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, + }, + { + login_challenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + getConsentRequest: async function (this: RecipeInterface, input): Promise { + const resp = await querier.sendGetRequest( + new NormalisedURLPath("/recipe/oauth/auth/requests/consent"), + { consentChallenge: input.challenge }, + input.userContext + ); + + return { + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptConsentRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/accept`), + { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + iss: await OpenIdRecipe.getIssuer(input.userContext), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, + initialAccessTokenPayload: input.initialAccessTokenPayload, + initialIdTokenPayload: input.initialIdTokenPayload, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + + rejectConsentRequest: async function (this: RecipeInterface, input) { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/reject`), + { + error: input.error.error, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + authorization: async function (this: RecipeInterface, input) { + // we handle this in the backend SDK level + if (input.params.prompt === "none") { + input.params["st_prompt"] = "none"; + delete input.params.prompt; + } + + let payloads: { idToken: JSONObject | undefined; accessToken: JSONObject | undefined } | undefined; + + if (input.params.client_id === undefined || typeof input.params.client_id !== "string") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required and must be a string", + }; + } + + const scopes = await this.getRequestedScopes({ + scopeParam: input.params.scope?.split(" ") || [], + clientId: input.params.client_id as string, + recipeUserId: input.session?.getRecipeUserId(), + sessionHandle: input.session?.getHandle(), + userContext: input.userContext, + }); + + const responseTypes = input.params.response_type?.split(" ") ?? []; + + if (input.session !== undefined) { + const clientInfo = await this.getOAuth2Client({ + clientId: input.params.client_id as string, + userContext: input.userContext, + }); + + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + + const user = await getUser(input.session.getUserId()); + if (!user) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "User deleted", + }; + } + + // These default to an empty objects, because we want to keep them as a required input + // but they'll not be actually used in the flows where we are not building them. + const idToken = + scopes.includes("openid") && (responseTypes.includes("id_token") || responseTypes.includes("code")) + ? await this.buildIdTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes, + userContext: input.userContext, + }) + : {}; + const accessToken = + responseTypes.includes("token") || responseTypes.includes("code") + ? await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes, + userContext: input.userContext, + }) + : {}; + payloads = { + idToken, + accessToken, + }; + } + + const resp = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/auth`), + { + params: { + ...input.params, + scope: scopes.join(" "), + }, + cookies: input.cookies, + session: payloads, + }, + input.userContext + ); + + if (resp.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "The provided client_id is not valid", + }; + } + + if (resp.status !== "OK") { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + if (redirectTo === undefined) { + throw new Error(resp.body); + } + const redirectToURL = new URL(redirectTo); + const consentChallenge = redirectToURL.searchParams.get("consent_challenge"); + if (consentChallenge !== null && input.session !== undefined) { + const consentRequest = await this.getConsentRequest({ + challenge: consentChallenge, + userContext: input.userContext, + }); + + const consentRes = await this.acceptConsentRequest({ + userContext: input.userContext, + challenge: consentRequest.challenge, + grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, + grantScope: consentRequest.requestedScope, + tenantId: input.session.getTenantId(), + rsub: input.session.getRecipeUserId().getAsString(), + sessionHandle: input.session.getHandle(), + initialAccessTokenPayload: payloads?.accessToken, + initialIdTokenPayload: payloads?.idToken, + }); + + return { + redirectTo: consentRes.redirectTo, + cookies: resp.cookies, + }; + } + return { redirectTo, cookies: resp.cookies }; + }, + + tokenExchange: async function (this: RecipeInterface, input) { + const body: any = { + inputBody: input.body, + authorizationHeader: input.authorizationHeader, + }; + + body.iss = await OpenIdRecipe.getIssuer(input.userContext); + + if (input.body.grant_type === "password") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "Unsupported grant type: password", + }; + } + + if (input.body.grant_type === "client_credentials") { + if (input.body.client_id === undefined) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required", + }; + } + + const scopes = input.body.scope?.split(" ") ?? []; + const clientInfo = await this.getOAuth2Client({ + clientId: input.body.client_id as string, + userContext: input.userContext, + }); + + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + body["id_token"] = await this.buildIdTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + body["access_token"] = await this.buildAccessTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + } + + if (input.body.grant_type === "refresh_token") { + const scopes = input.body.scope?.split(" ") ?? []; + const tokenInfo = await this.introspectToken({ + token: input.body.refresh_token!, + scopes, + userContext: input.userContext, + }); + + if (tokenInfo.active === true) { + const sessionHandle = tokenInfo.sessionHandle as string; + + const clientInfo = await this.getOAuth2Client({ + clientId: tokenInfo.client_id as string, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorDescription, + }; + } + const client = clientInfo.client; + const user = await getUser(tokenInfo.sub as string); + if (!user) { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "User not found", + }; + } + body["id_token"] = await this.buildIdTokenPayload({ + user, + client, + sessionHandle, + scopes, + userContext: input.userContext, + }); + body["access_token"] = await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + } + } + + if (input.authorizationHeader) { + body["authorizationHeader"] = input.authorizationHeader; + } + + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/token`), + body, + input.userContext + ); + + if (res.status !== "OK") { + return { + status: "ERROR", + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + return res; + }, + + getOAuth2Clients: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new NormalisedURLPath(`/recipe/oauth/clients/list`), + { + pageSize: input.pageSize, + clientName: input.clientName, + pageToken: input.paginationToken, + }, + {}, + input.userContext + ); + + if (response.body.status === "OK") { + return { + status: "OK", + clients: response.body.clients.map((client: any) => OAuth2Client.fromAPIResponse(client)), + nextPaginationToken: response.body.nextPaginationToken, + }; + } else { + return { + status: "ERROR", + error: response.body.error, + errorDescription: response.body.errorDescription, + }; + } + }, + getOAuth2Client: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new NormalisedURLPath(`/recipe/oauth/clients`), + { clientId: input.clientId }, + {}, + input.userContext + ); + + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.body), + }; + } else if (response.body.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + error: "invalid_request", + errorDescription: "The provided client_id is not valid or unknown", + }; + } else { + return { + status: "ERROR", + error: response.body.error, + errorDescription: response.body.errorDescription, + }; + } + }, + createOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + input.userContext + ); + + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response), + }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + updateOAuth2Client: async function (input) { + let response = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + { clientId: input.clientId }, + input.userContext + ); + + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response), + }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + deleteOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/clients/remove`), + { clientId: input.clientId }, + input.userContext + ); + + if (response.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "ERROR", + error: response.error, + errorDescription: response.errorDescription, + }; + } + }, + getRequestedScopes: async function (this: RecipeInterface, input): Promise { + return input.scopeParam; + }, + buildAccessTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } + return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildIdTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } + return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); + }, + getFrontendRedirectionURL: async function (input) { + const websiteDomain = appInfo + .getOrigin({ request: undefined, userContext: input.userContext }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.tenantId !== undefined && input.tenantId !== DEFAULT_TENANT_ID) { + queryParams.set("tenantId", input.tenantId); + } + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + + return `${websiteDomain}${websiteBasePath}?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `${websiteDomain}${websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `${websiteDomain}${websiteBasePath}`; + } else if (input.type === "logout-confirmation") { + return `${websiteDomain}${websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`; + } + + throw new Error("This should never happen: invalid type passed to getFrontendRedirectionURL"); + }, + validateOAuth2AccessToken: async function (input) { + const payload = ( + await jose.jwtVerify(input.token, getCombinedJWKS(SessionRecipe.getInstanceOrThrowError().config)) + ).payload; + + if (payload.stt !== 1) { + throw new Error("Wrong token type"); + } + + if (input.requirements?.clientId !== undefined && payload.client_id !== input.requirements.clientId) { + throw new Error( + `The token doesn't belong to the specified client (${input.requirements.clientId} !== ${payload.client_id})` + ); + } + + if ( + input.requirements?.scopes !== undefined && + input.requirements.scopes.some((scope) => !(payload.scp as string[]).includes(scope)) + ) { + throw new Error("The token is missing some required scopes"); + } + + const aud = payload.aud instanceof Array ? payload.aud : [payload.aud]; + if (input.requirements?.audience !== undefined && !aud.includes(input.requirements.audience)) { + throw new Error("The token doesn't belong to the specified audience"); + } + + if (input.checkDatabase) { + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/introspect`), + { + token: input.token, + }, + input.userContext + ); + + if (response.active !== true) { + throw new Error("The token is expired, invalid or has been revoked"); + } + } + return { status: "OK", payload: payload as JSONObject }; + }, + revokeToken: async function (this: RecipeInterface, input) { + const requestBody: Record = { + token: input.token, + }; + + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input && input.clientId !== undefined) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input && input.clientSecret !== undefined) { + requestBody.client_secret = input.clientSecret; + } + } + + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/token/revoke`), + requestBody, + input.userContext + ); + + if (res.status !== "OK") { + return { + status: "ERROR", + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + + return { status: "OK" }; + }, + revokeTokensBySessionHandle: async function (this: RecipeInterface, input) { + await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/session/revoke`), + { sessionHandle: input.sessionHandle }, + input.userContext + ); + + return { status: "OK" }; + }, + revokeTokensByClientId: async function (this: RecipeInterface, input) { + await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/tokens/revoke`), + { clientId: input.clientId }, + input.userContext + ); + + return { status: "OK" }; + }, + + introspectToken: async function (this: RecipeInterface, { token, scopes, userContext }) { + // Determine if the token is an access token by checking if it doesn't start with "st_rt" + const isAccessToken = !token.startsWith("st_rt"); + + // Attempt to validate the access token locally + // If it fails, the token is not active, and we return early + if (isAccessToken) { + try { + await this.validateOAuth2AccessToken({ + token, + requirements: { scopes }, + checkDatabase: false, + userContext, + }); + } catch (error) { + return { active: false }; + } + } + + // For tokens that passed local validation or if it's a refresh token, + // validate the token with the database by calling the core introspection endpoint + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/introspect`), + { + token, + scope: scopes ? scopes.join(" ") : undefined, + }, + userContext + ); + + return res; + }, + + endSession: async function (this: RecipeInterface, input) { + /** + * NOTE: The API response has 3 possible cases: + * + * CASE 1: `end_session` request with a valid `id_token_hint` + * - Redirects to `/oauth/logout` with a `logout_challenge`. + * + * CASE 2: `end_session` request with an already logged out `id_token_hint` + * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. + * + * CASE 3: `end_session` request with a `logout_verifier` (after accepting the logout request) + * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. + */ + + const resp = await querier.sendGetRequest( + new NormalisedURLPath(`/recipe/oauth/sessions/logout`), + { + clientId: input.params.client_id, + idTokenHint: input.params.id_token_hint, + postLogoutRedirectUri: input.params.post_logout_redirect_uri, + state: input.params.state, + logoutVerifier: input.params.logout_verifier, + }, + input.userContext + ); + + if ("error" in resp) { + return { + status: "ERROR", + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; + } + let redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + + const initialRedirectToURL = new URL(redirectTo); + const logoutChallenge = initialRedirectToURL.searchParams.get("logout_challenge"); + + // CASE 1 (See above notes) + if (logoutChallenge !== null) { + // Redirect to the frontend to ask for logout confirmation if there is a valid or expired supertokens session + if (input.session !== undefined || input.shouldTryRefresh) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "logout-confirmation", + logoutChallenge, + userContext: input.userContext, + }), + }; + } else { + // Accept the logout challenge immediately as there is no supertokens session + redirectTo = ( + await this.acceptLogoutRequest({ + challenge: logoutChallenge, + userContext: input.userContext, + }) + ).redirectTo; + } + } + + // CASE 2 or 3 (See above notes) + + // NOTE: If no post_logout_redirect_uri is provided, Hydra redirects to a fallback page. + // In this case, we redirect the user to the /auth page. + if (redirectTo.endsWith("/fallbacks/logout/callback")) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; + } + + return { redirectTo }; + }, + acceptLogoutRequest: async function (this: RecipeInterface, input) { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/logout/accept`), + { challenge: input.challenge }, + {}, + input.userContext + ); + + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + + if (redirectTo.endsWith("/fallbacks/logout/callback")) { + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; + } + + return { redirectTo }; + }, + rejectLogoutRequest: async function (this: RecipeInterface, input) { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/logout/reject`), + {}, + { challenge: input.challenge }, + input.userContext + ); + + if (resp.status != "OK") { + throw new Error(resp.error); + } + + return { status: "OK" }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts new file mode 100644 index 000000000..c964f7ce0 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -0,0 +1,593 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; +import { SessionContainerInterface } from "../session/types"; +import { OAuth2Client } from "./OAuth2Client"; +import { User } from "../../user"; +import RecipeUserId from "../../recipeUserId"; + +export type TypeInput = { + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type TypeNormalisedInput = { + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; +}; + +export type ErrorOAuth2 = { + status: "ERROR"; + + // The error should follow the OAuth2 error format (e.g. invalid_request, login_required). + // Defaults to request_denied. + error: string; + + // Description of the error in a human readable format. + errorDescription: string; + + // Represents the HTTP status code of the error (e.g. 401 or 403) + // Defaults to 400 + statusCode?: number; +}; + +export type ConsentRequest = { + // ACR represents the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it to express that, for example, a user authenticated using two factor authentication. + acr?: string; + + // Array of strings + amr?: string[]; + + // ID is the identifier ("authorization challenge") of the consent authorization request. It is used to identify the session. + challenge: string; + + // OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. + client?: OAuth2Client; + + // any json serializable object + context?: JSONObject; + + // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. + loginChallenge?: string; + + // LoginSessionID is the login session ID. + loginSessionId?: string; + + // object (Contains optional information about the OpenID Connect request.) + oidcContext?: any; + + // Array of strings + requestedAccessTokenAudience?: string[]; + + // Array of strings + requestedScope?: string[]; + + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the consent request using the usual API call. + skip?: boolean; + + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. + subject?: string; +}; + +export type LoginRequest = { + // ID is the identifier ("login challenge") of the login request. It is used to identify the session. + challenge: string; + + // OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. + client: OAuth2Client; + + // object (Contains optional information about the OpenID Connect request.) + oidcContext?: any; + + // RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. + requestUrl: string; + + // Array of strings + requestedAccessTokenAudience?: string[]; + + // Array of strings + requestedScope?: string[]; + + // SessionID is the login session ID. + sessionId?: string; + + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. + // This feature allows you to update / set session information. + skip: boolean; + + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. If this value is set and skip is true, you MUST include this subject type when accepting the login request, or the request will fail. + subject: string; +}; + +export type TokenInfo = { + // The access token issued by the authorization server. + access_token?: string; + // The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. + // integer + expires_in: number; + // To retrieve a refresh token request the id_token scope. + id_token?: string; + // The refresh token, which can be used to obtain new access tokens. To retrieve it add the scope "offline" to your access token request. + refresh_token?: string; + // The scope of the access token + scope: string; + // The type of the token issued + token_type: string; +}; + +export type LoginInfo = { + clientId: string; + // The name of the client. + clientName: string; + // The URI of the client's terms of service. + tosUri?: string; + // The URI of the client's privacy policy. + policyUri?: string; + // The URI of the client's logo. + logoUri?: string; + // The URI of the client + clientUri?: string; + // The metadata associated with the client. + metadata?: Record | null; +}; + +export type UserInfo = { + sub: string; + email?: string; + email_verified?: boolean; + phoneNumber?: string; + phoneNumber_verified?: boolean; + [key: string]: JSONValue; +}; + +export type InstrospectTokenResponse = { active: false } | ({ active: true } & JSONObject); + +export type RecipeInterface = { + authorization(input: { + params: Record; + cookies: string | undefined; + session: SessionContainerInterface | undefined; + userContext: UserContext; + }): Promise<{ redirectTo: string; cookies: string | undefined } | ErrorOAuth2>; + tokenExchange(input: { + authorizationHeader?: string; + body: Record; + userContext: UserContext; + }): Promise; + getConsentRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptConsentRequest(input: { + challenge: string; + + // any json serializable object + context?: any; + // Array of strings + grantAccessTokenAudience?: string[]; + // Array of strings + grantScope?: string[]; + // string (NullTime implements sql.NullTime functionality.) + handledAt?: string; + + tenantId: string; + rsub: string; + sessionHandle: string; + initialAccessTokenPayload: JSONObject | undefined; + initialIdTokenPayload: JSONObject | undefined; + + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + rejectConsentRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + getLoginRequest(input: { + challenge: string; + userContext: UserContext; + }): Promise<(LoginRequest & { status: "OK" }) | ErrorOAuth2>; + acceptLoginRequest(input: { + challenge: string; + + // ACR sets the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it to express that, for example, a user authenticated using two factor authentication. + acr?: string; + + // Array of strings + amr?: string[]; + + // any json serializable object + context?: any; + + // Extend OAuth2 authentication session lifespan + // If set to true, the OAuth2 authentication cookie lifespan is extended. This is for example useful if you want the user to be able to use prompt=none continuously. + // This value can only be set to true if the user has an authentication, which is the case if the skip value is true. + extendSessionLifespan?: boolean; + + // IdentityProviderSessionID is the session ID of the end-user that authenticated. + identityProviderSessionId?: string; + + // Subject is the user ID of the end-user that authenticated. + subject: string; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + rejectLoginRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + getOAuth2Client(input: { + clientId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + getOAuth2Clients( + input: GetOAuth2ClientsInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + clients: Array; + nextPaginationToken?: string; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + createOAuth2Client( + input: CreateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + updateOAuth2Client( + input: UpdateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + deleteOAuth2Client( + input: DeleteOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + } + | { + status: "ERROR"; + error: string; + errorDescription: string; + } + >; + + validateOAuth2AccessToken(input: { + token: string; + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }; + checkDatabase?: boolean; + userContext: UserContext; + }): Promise<{ status: "OK"; payload: JSONObject }>; + + getRequestedScopes(input: { + recipeUserId: RecipeUserId | undefined; + sessionHandle: string | undefined; + scopeParam: string[]; + clientId: string; + userContext: UserContext; + }): Promise; + buildAccessTokenPayload(input: { + user: User | undefined; + client: OAuth2Client; + sessionHandle: string | undefined; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildIdTokenPayload(input: { + user: User | undefined; + client: OAuth2Client; + sessionHandle: string | undefined; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildUserInfo(input: { + user: User; + accessTokenPayload: JSONObject; + scopes: string[]; + tenantId: string; + userContext: UserContext; + }): Promise; + getFrontendRedirectionURL( + input: + | { + type: "login"; + loginChallenge: string; + tenantId: string; + forceFreshAuth: boolean; + hint: string | undefined; + userContext: UserContext; + } + | { + type: "try-refresh"; + loginChallenge: string; + userContext: UserContext; + } + | { + type: "logout-confirmation"; + logoutChallenge: string; + userContext: UserContext; + } + | { + type: "post-logout-fallback"; + userContext: UserContext; + } + ): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { clientId: string; clientSecret?: string } + ) + ): Promise<{ status: "OK" } | ErrorOAuth2>; + revokeTokensByClientId(input: { clientId: string; userContext: UserContext }): Promise<{ status: "OK" }>; + revokeTokensBySessionHandle(input: { sessionHandle: string; userContext: UserContext }): Promise<{ status: "OK" }>; + introspectToken(input: { + token: string; + scopes?: string[]; + userContext: UserContext; + }): Promise; + endSession(input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }): Promise<{ redirectTo: string } | ErrorOAuth2>; + acceptLogoutRequest(input: { challenge: string; userContext: UserContext }): Promise<{ redirectTo: string }>; + rejectLogoutRequest(input: { challenge: string; userContext: UserContext }): Promise<{ status: "OK" }>; +}; + +export type APIInterface = { + loginGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }) => Promise<{ frontendRedirectTo: string; cookies?: string } | ErrorOAuth2 | GeneralErrorResponse>); + + authGET: + | undefined + | ((input: { + params: any; + cookie: string | undefined; + session: SessionContainerInterface | undefined; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ redirectTo: string; cookies?: string } | ErrorOAuth2 | GeneralErrorResponse>); + tokenPOST: + | undefined + | ((input: { + authorizationHeader?: string; + body: any; + options: APIOptions; + userContext: UserContext; + }) => Promise); + loginInfoGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ status: "OK"; info: LoginInfo } | ErrorOAuth2 | GeneralErrorResponse>); + userInfoGET: + | undefined + | ((input: { + accessTokenPayload: JSONObject; + user: User; + scopes: string[]; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ({ authorizationHeader: string } | { clientId: string; clientSecret?: string }) + ) => Promise<{ status: "OK" } | ErrorOAuth2>); + introspectTokenPOST: + | undefined + | ((input: { + token: string; + scopes?: string[]; + options: APIOptions; + userContext: UserContext; + }) => Promise); + endSessionGET: + | undefined + | ((input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ redirectTo: string } | ErrorOAuth2 | GeneralErrorResponse>); + endSessionPOST: + | undefined + | ((input: { + params: Record; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ redirectTo: string } | ErrorOAuth2 | GeneralErrorResponse>); + logoutPOST: + | undefined + | ((input: { + logoutChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + userContext: UserContext; + }) => Promise<{ status: "OK"; frontendRedirectTo: string } | ErrorOAuth2 | GeneralErrorResponse>); +}; + +export type OAuth2ClientOptions = { + clientId: string; + clientSecret?: string; + createdAt: string; + updatedAt: string; + + clientName: string; + + scope: string; + redirectUris?: string[] | null; + postLogoutRedirectUris?: string[]; + + authorizationCodeGrantAccessTokenLifespan?: string | null; + authorizationCodeGrantIdTokenLifespan?: string | null; + authorizationCodeGrantRefreshTokenLifespan?: string | null; + clientCredentialsGrantAccessTokenLifespan?: string | null; + implicitGrantAccessTokenLifespan?: string | null; + implicitGrantIdTokenLifespan?: string | null; + refreshTokenGrantAccessTokenLifespan?: string | null; + refreshTokenGrantIdTokenLifespan?: string | null; + refreshTokenGrantRefreshTokenLifespan?: string | null; + + tokenEndpointAuthMethod: string; + + audience?: string[]; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + + clientUri?: string; + logoUri?: string; + policyUri?: string; + tosUri?: string; + metadata?: Record; +}; + +export type GetOAuth2ClientsInput = { + /** + * Items per Page. Defaults to 250. + */ + pageSize?: number; + + /** + * Next Page Token. Defaults to "1". + */ + paginationToken?: string; + + /** + * The name of the clients to filter by. + */ + clientName?: string; +}; + +export type CreateOAuth2ClientInput = Partial< + Omit +>; + +export type UpdateOAuth2ClientInput = NonNullableProperties< + Omit +> & { + clientId: string; + redirectUris?: string[] | null; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + metadata?: Record | null; +}; + +export type DeleteOAuth2ClientInput = { + clientId: string; +}; + +export type PayloadBuilderFunction = ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext +) => Promise; +export type UserInfoBuilderFunction = ( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext +) => Promise; diff --git a/lib/ts/recipe/oauth2provider/utils.ts b/lib/ts/recipe/oauth2provider/utils.ts new file mode 100644 index 000000000..38b77db1b --- /dev/null +++ b/lib/ts/recipe/oauth2provider/utils.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from "../../types"; +import Recipe from "./recipe"; +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; + +export function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput +): TypeNormalisedInput { + let override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + }; + + return { + override, + }; +} diff --git a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts index 90c291574..466a433f9 100644 --- a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts +++ b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts @@ -34,6 +34,15 @@ export default async function getOpenIdDiscoveryConfiguration( send200Response(options.res, { issuer: result.issuer, jwks_uri: result.jwks_uri, + authorization_endpoint: result.authorization_endpoint, + token_endpoint: result.token_endpoint, + userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, + token_introspection_endpoint: result.token_introspection_endpoint, + end_session_endpoint: result.end_session_endpoint, + subject_types_supported: result.subject_types_supported, + id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, + response_types_supported: result.response_types_supported, }); } else { send200Response(options.res, result); diff --git a/lib/ts/recipe/openid/api/implementation.ts b/lib/ts/recipe/openid/api/implementation.ts index ee1f83e21..72921dffd 100644 --- a/lib/ts/recipe/openid/api/implementation.ts +++ b/lib/ts/recipe/openid/api/implementation.ts @@ -12,18 +12,11 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { APIInterface, APIOptions } from "../types"; -import { GeneralErrorResponse, UserContext } from "../../../types"; +import { APIInterface } from "../types"; export default function getAPIImplementation(): APIInterface { return { - getOpenIdDiscoveryConfigurationGET: async function ({ - options, - userContext, - }: { - options: APIOptions; - userContext: UserContext; - }): Promise<{ status: "OK"; issuer: string; jwks_uri: string } | GeneralErrorResponse> { + getOpenIdDiscoveryConfigurationGET: async function ({ options, userContext }) { return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); }, }; diff --git a/lib/ts/recipe/openid/index.ts b/lib/ts/recipe/openid/index.ts index cf69bc4b6..926e2e98b 100644 --- a/lib/ts/recipe/openid/index.ts +++ b/lib/ts/recipe/openid/index.ts @@ -9,29 +9,7 @@ export default class OpenIdRecipeWrapper { userContext: getUserContext(userContext), }); } - - static createJWT( - payload?: any, - validitySeconds?: number, - useStaticSigningKey?: boolean, - userContext?: Record - ) { - return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - useStaticSigningKey, - userContext: getUserContext(userContext), - }); - } - - static getJWKS(userContext?: Record) { - return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: getUserContext(userContext), - }); - } } export let init = OpenIdRecipeWrapper.init; export let getOpenIdDiscoveryConfiguration = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -export let createJWT = OpenIdRecipeWrapper.createJWT; -export let getJWKS = OpenIdRecipeWrapper.getJWKS; diff --git a/lib/ts/recipe/openid/recipe.ts b/lib/ts/recipe/openid/recipe.ts index 6d2d7e4af..07c9af3e1 100644 --- a/lib/ts/recipe/openid/recipe.ts +++ b/lib/ts/recipe/openid/recipe.ts @@ -19,7 +19,6 @@ import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; -import JWTRecipe from "../jwt/recipe"; import OverrideableBuilder from "supertokens-js-override"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; @@ -32,20 +31,15 @@ export default class OpenIdRecipe extends RecipeModule { static RECIPE_ID = "openid"; private static instance: OpenIdRecipe | undefined = undefined; config: TypeNormalisedInput; - jwtRecipe: JWTRecipe; recipeImplementation: RecipeInterface; apiImpl: APIInterface; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + constructor(recipeId: string, appInfo: NormalisedAppinfo, config?: TypeInput) { super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(appInfo, config); - this.jwtRecipe = new JWTRecipe(recipeId, appInfo, isInServerlessEnv, { - jwtValiditySeconds: this.config.jwtValiditySeconds, - override: this.config.override.jwtFeature, - }); + this.config = validateAndNormaliseUserInput(config); - let builder = new OverrideableBuilder(RecipeImplementation(this.config, this.jwtRecipe.recipeInterfaceImpl)); + let builder = new OverrideableBuilder(RecipeImplementation(appInfo)); this.recipeImplementation = builder.override(this.config.override.functions).build(); @@ -62,9 +56,9 @@ export default class OpenIdRecipe extends RecipeModule { } static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { + return (appInfo) => { if (OpenIdRecipe.instance === undefined) { - OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, config); return OpenIdRecipe.instance; } else { throw new Error("OpenId recipe has already been initialised. Please check your code for bugs."); @@ -79,6 +73,12 @@ export default class OpenIdRecipe extends RecipeModule { OpenIdRecipe.instance = undefined; } + static async getIssuer(userContext: UserContext) { + return ( + await this.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }) + ).issuer; + } + getAPIsHandled = (): APIHandled[] => { return [ { @@ -87,16 +87,15 @@ export default class OpenIdRecipe extends RecipeModule { id: GET_DISCOVERY_CONFIG_URL, disabled: this.apiImpl.getOpenIdDiscoveryConfigurationGET === undefined, }, - ...this.jwtRecipe.getAPIsHandled(), ]; }; handleAPIRequest = async ( id: string, - tenantId: string, + _tenantId: string, req: BaseRequest, response: BaseResponse, - path: normalisedURLPath, - method: HTTPMethod, + _path: normalisedURLPath, + _method: HTTPMethod, userContext: UserContext ): Promise => { let apiOptions: APIOptions = { @@ -110,28 +109,16 @@ export default class OpenIdRecipe extends RecipeModule { if (id === GET_DISCOVERY_CONFIG_URL) { return await getOpenIdDiscoveryConfiguration(this.apiImpl, apiOptions, userContext); } else { - return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); + return false; } }; - handleError = async ( - error: STError, - request: BaseRequest, - response: BaseResponse, - userContext: UserContext - ): Promise => { - if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { - throw error; - } else { - return await this.jwtRecipe.handleError(error, request, response, userContext); - } + handleError = async (error: STError): Promise => { + throw error; }; getAllCORSHeaders = (): string[] => { - return [...this.jwtRecipe.getAllCORSHeaders()]; + return []; }; isErrorFromThisRecipe = (err: any): err is STError => { - return ( - (STError.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID) || - this.jwtRecipe.isErrorFromThisRecipe(err) - ); + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID; }; } diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts index f161e8d23..5fa6751a3 100644 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ b/lib/ts/recipe/openid/recipeImplementation.ts @@ -12,43 +12,58 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { RecipeInterface, TypeNormalisedInput } from "./types"; -import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types"; +import { RecipeInterface } from "./types"; +import JWTRecipe from "../jwt/recipe"; import NormalisedURLPath from "../../normalisedURLPath"; import { GET_JWKS_API } from "../jwt/constants"; -import { UserContext } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; +import { + AUTH_PATH, + END_SESSION_PATH, + INTROSPECT_TOKEN_PATH, + REVOKE_TOKEN_PATH, + TOKEN_PATH, + USER_INFO_PATH, +} from "../oauth2provider/constants"; -export default function getRecipeInterface( - config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface -): RecipeInterface { +export default function getRecipeInterface(appInfo: NormalisedAppinfo): RecipeInterface { return { - getOpenIdDiscoveryConfiguration: async function (): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }> { - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); + getOpenIdDiscoveryConfiguration: async function () { + let issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); let jwks_uri = - config.issuerDomain.getAsStringDangerous() + - config.issuerPath.appendPath(new NormalisedURLPath(GET_JWKS_API)).getAsStringDangerous(); + appInfo.apiDomain.getAsStringDangerous() + + appInfo.apiBasePath.appendPath(new NormalisedURLPath(GET_JWKS_API)).getAsStringDangerous(); + + const apiBasePath = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); return { status: "OK", issuer, jwks_uri, + authorization_endpoint: apiBasePath + AUTH_PATH, + token_endpoint: apiBasePath + TOKEN_PATH, + userinfo_endpoint: apiBasePath + USER_INFO_PATH, + revocation_endpoint: apiBasePath + REVOKE_TOKEN_PATH, + token_introspection_endpoint: apiBasePath + INTROSPECT_TOKEN_PATH, + end_session_endpoint: apiBasePath + END_SESSION_PATH, + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + response_types_supported: ["code", "id_token", "id_token token"], }; }, - createJWT: async function ({ - payload, - validitySeconds, - useStaticSigningKey, - userContext, - }: { - payload?: any; - validitySeconds?: number; - useStaticSigningKey?: boolean; - userContext: UserContext; - }): Promise< + createJWT: async function ( + this: RecipeInterface, + { + payload, + validitySeconds, + useStaticSigningKey, + userContext, + }: { + payload?: any; + validitySeconds?: number; + useStaticSigningKey?: boolean; + userContext: UserContext; + } + ): Promise< | { status: "OK"; jwt: string; @@ -59,8 +74,8 @@ export default function getRecipeInterface( > { payload = payload === undefined || payload === null ? {} : payload; - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - return await jwtRecipeImplementation.createJWT({ + let issuer = (await this.getOpenIdDiscoveryConfiguration({ userContext })).issuer; + return await JWTRecipe.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ payload: { iss: issuer, ...payload, @@ -70,8 +85,5 @@ export default function getRecipeInterface( userContext, }); }, - getJWKS: async function (input): Promise<{ keys: JsonWebKey[] }> { - return await jwtRecipeImplementation.getJWKS(input); - }, }; } diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts index c303cb0ca..11babcb13 100644 --- a/lib/ts/recipe/openid/types.ts +++ b/lib/ts/recipe/openid/types.ts @@ -14,53 +14,25 @@ */ import OverrideableBuilder from "supertokens-js-override"; import type { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; import { GeneralErrorResponse, UserContext } from "../../types"; export type TypeInput = { - issuer?: string; - jwtValiditySeconds?: number; override?: { functions?: ( originalImplementation: RecipeInterface, builder?: OverrideableBuilder ) => RecipeInterface; apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; }; }; export type TypeNormalisedInput = { - issuerDomain: NormalisedURLDomain; - issuerPath: NormalisedURLPath; - jwtValiditySeconds?: number; override: { functions: ( originalImplementation: RecipeInterface, builder?: OverrideableBuilder ) => RecipeInterface; apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; }; }; @@ -83,6 +55,15 @@ export type APIInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; } | GeneralErrorResponse >); @@ -95,6 +76,15 @@ export type RecipeInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + end_session_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; createJWT(input: { payload?: any; @@ -110,10 +100,4 @@ export type RecipeInterface = { status: "UNSUPPORTED_ALGORITHM_ERROR"; } >; - - getJWKS(input: { - userContext: UserContext; - }): Promise<{ - keys: JsonWebKey[]; - }>; }; diff --git a/lib/ts/recipe/openid/utils.ts b/lib/ts/recipe/openid/utils.ts index 72414d9c9..0950214e3 100644 --- a/lib/ts/recipe/openid/utils.ts +++ b/lib/ts/recipe/openid/utils.ts @@ -12,26 +12,9 @@ * License for the specific language governing permissions and limitations * under the License. */ -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { NormalisedAppinfo } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput { - let issuerDomain = appInfo.apiDomain; - let issuerPath = appInfo.apiBasePath; - - if (config !== undefined) { - if (config.issuer !== undefined) { - issuerDomain = new NormalisedURLDomain(config.issuer); - issuerPath = new NormalisedURLPath(config.issuer); - } - - if (!issuerPath.equals(appInfo.apiBasePath)) { - throw new Error("The path of the issuer URL must be equal to the apiBasePath. The default value is /auth"); - } - } - +export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { let override = { functions: (originalImplementation: RecipeInterface) => originalImplementation, apis: (originalImplementation: APIInterface) => originalImplementation, @@ -39,9 +22,6 @@ export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config }; return { - issuerDomain, - issuerPath, - jwtValiditySeconds: config?.jwtValiditySeconds, override, }; } diff --git a/lib/ts/recipe/passwordless/api/consumeCode.ts b/lib/ts/recipe/passwordless/api/consumeCode.ts index 38aa06c4b..39229cca7 100644 --- a/lib/ts/recipe/passwordless/api/consumeCode.ts +++ b/lib/ts/recipe/passwordless/api/consumeCode.ts @@ -13,11 +13,15 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function consumeCode( apiImplementation: APIInterface, @@ -62,13 +66,12 @@ export default async function consumeCode( }); } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -84,6 +87,7 @@ export default async function consumeCode( preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, } @@ -93,6 +97,7 @@ export default async function consumeCode( preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, } ); diff --git a/lib/ts/recipe/passwordless/api/createCode.ts b/lib/ts/recipe/passwordless/api/createCode.ts index af46ff86d..246742c8c 100644 --- a/lib/ts/recipe/passwordless/api/createCode.ts +++ b/lib/ts/recipe/passwordless/api/createCode.ts @@ -13,12 +13,12 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getNormalisedShouldTryLinkingWithSessionUserFlag, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import parsePhoneNumber from "libphonenumber-js/max"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function createCode( apiImplementation: APIInterface, @@ -93,13 +93,12 @@ export default async function createCode( } } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -109,8 +108,8 @@ export default async function createCode( let result = await apiImplementation.createCodePOST( email !== undefined - ? { email, session, tenantId, options, userContext } - : { phoneNumber: phoneNumber!, session, tenantId, options, userContext } + ? { email, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } + : { phoneNumber: phoneNumber!, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } ); send200Response(options.res, result); diff --git a/lib/ts/recipe/passwordless/api/implementation.ts b/lib/ts/recipe/passwordless/api/implementation.ts index 7b7894bb8..c769ad6ea 100644 --- a/lib/ts/recipe/passwordless/api/implementation.ts +++ b/lib/ts/recipe/passwordless/api/implementation.ts @@ -179,6 +179,7 @@ export default function getAPIImplementation(): APIInterface { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -207,6 +208,7 @@ export default function getAPIImplementation(): APIInterface { deviceId: input.deviceId, userInputCode: input.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -214,6 +216,7 @@ export default function getAPIImplementation(): APIInterface { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -319,6 +322,7 @@ export default function getAPIImplementation(): APIInterface { factorIds, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -344,6 +348,7 @@ export default function getAPIImplementation(): APIInterface { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } : { @@ -357,12 +362,13 @@ export default function getAPIImplementation(): APIInterface { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } ); if (response.status !== "OK") { - return AuthUtils.getErrorStatusResponseWithReason(response, {}, "SIGN_IN_UP_NOT_ALLOWED"); + return AuthUtils.getErrorStatusResponseWithReason(response, errorCodeMap, "SIGN_IN_UP_NOT_ALLOWED"); } // now we send the email / text message. @@ -500,6 +506,7 @@ export default function getAPIImplementation(): APIInterface { }); const authTypeInfo = await AuthUtils.checkAuthTypeAndLinkingStatus( input.session, + input.shouldTryLinkingWithSessionUser, { recipeId: "passwordless", email: deviceInfo.email, @@ -553,7 +560,7 @@ export default function getAPIImplementation(): APIInterface { // This mirrors how we construct factorIds in createCodePOST let factorIds; - if (input.session !== undefined) { + if (!authTypeInfo.isFirstFactor) { if (deviceInfo.email !== undefined) { factorIds = [FactorIds.OTP_EMAIL]; } else { diff --git a/lib/ts/recipe/passwordless/api/resendCode.ts b/lib/ts/recipe/passwordless/api/resendCode.ts index cb946546a..1f1abe48d 100644 --- a/lib/ts/recipe/passwordless/api/resendCode.ts +++ b/lib/ts/recipe/passwordless/api/resendCode.ts @@ -13,11 +13,11 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getNormalisedShouldTryLinkingWithSessionUserFlag, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function resendCode( apiImplementation: APIInterface, @@ -47,25 +47,21 @@ export default async function resendCode( }); } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { - tenantId = session.getTenantId(); - } - let result = await apiImplementation.resendCodePOST({ deviceId, preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts index 92b15e493..43d00fd2a 100644 --- a/lib/ts/recipe/passwordless/index.ts +++ b/lib/ts/recipe/passwordless/index.ts @@ -51,6 +51,7 @@ export default class Wrapper { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ ...input, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: getUserContext(input.userContext), }); } @@ -203,6 +204,7 @@ export default class Wrapper { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ ...input, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: getUserContext(input.userContext), }); } diff --git a/lib/ts/recipe/passwordless/recipe.ts b/lib/ts/recipe/passwordless/recipe.ts index 3ee073911..8ad1c11c4 100644 --- a/lib/ts/recipe/passwordless/recipe.ts +++ b/lib/ts/recipe/passwordless/recipe.ts @@ -578,6 +578,7 @@ export default class Recipe extends RecipeModule { email: input.email, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -585,6 +586,7 @@ export default class Recipe extends RecipeModule { phoneNumber: input.phoneNumber, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -636,12 +638,14 @@ export default class Recipe extends RecipeModule { email: input.email, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } : { phoneNumber: input.phoneNumber, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } ); @@ -656,6 +660,7 @@ export default class Recipe extends RecipeModule { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -664,6 +669,7 @@ export default class Recipe extends RecipeModule { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } diff --git a/lib/ts/recipe/passwordless/recipeImplementation.ts b/lib/ts/recipe/passwordless/recipeImplementation.ts index 7f742fd51..1732a61b8 100644 --- a/lib/ts/recipe/passwordless/recipeImplementation.ts +++ b/lib/ts/recipe/passwordless/recipeImplementation.ts @@ -43,11 +43,12 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { // Attempt account linking (this is a sign up) let updatedUser = response.user; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId: input.tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, userContext: input.userContext, }); @@ -188,6 +189,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { let response = await querier.sendPutRequest( new NormalisedURLPath(`/recipe/user`), copyAndRemoveUserContextAndTenantId(input), + {}, input.userContext ); if (response.status !== "OK") { diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 317d809ef..cbbf6f95f 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -119,6 +119,7 @@ export type RecipeInterface = { ) & { userInputCode?: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -158,6 +159,7 @@ export type RecipeInterface = { deviceId: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -165,6 +167,7 @@ export type RecipeInterface = { linkCode: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -334,6 +337,7 @@ export type APIInterface = { input: ({ email: string } | { phoneNumber: string }) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -355,6 +359,7 @@ export type APIInterface = { input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -374,6 +379,7 @@ export type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } diff --git a/lib/ts/recipe/session/accessToken.ts b/lib/ts/recipe/session/accessToken.ts index c9f5bc637..8c47ef6f6 100644 --- a/lib/ts/recipe/session/accessToken.ts +++ b/lib/ts/recipe/session/accessToken.ts @@ -124,6 +124,10 @@ export async function getInfoFromAccessToken( } export function validateAccessTokenStructure(payload: any, version: number) { + if (payload.stt !== 0 && payload.stt !== undefined) { + throw Error("Wrong token type"); + } + if (version >= 5) { if ( typeof payload.sub !== "string" || diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index 7a2a78498..e7a5c296a 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -34,4 +34,5 @@ export const protectedProps = [ "antiCsrfToken", "rsub", "tId", + "stt", ]; diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts index 0df3d7fe0..683ff99a8 100644 --- a/lib/ts/recipe/session/index.ts +++ b/lib/ts/recipe/session/index.ts @@ -26,6 +26,8 @@ import { RecipeInterface, } from "./types"; import Recipe from "./recipe"; +import OpenIdRecipe from "../openid/recipe"; +import JWTRecipe from "../jwt/recipe"; import { JSONObject, UserContext } from "../../types"; import { getRequiredClaimValidators } from "./utils"; import { createNewSessionInRequest, getSessionFromRequest, refreshSessionInRequest } from "./sessionRequestFunctions"; @@ -85,8 +87,7 @@ export default class SessionWrapper { const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const appInfo = recipeInstance.getAppInfo(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + const issuer = await OpenIdRecipe.getIssuer(ctx); let finalAccessTokenPayload = { ...accessTokenPayload, @@ -398,7 +399,7 @@ export default class SessionWrapper { useStaticSigningKey?: boolean, userContext?: Record ) { - return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.createJWT({ + return OpenIdRecipe.getInstanceOrThrowError().recipeImplementation.createJWT({ payload, validitySeconds, useStaticSigningKey, @@ -407,13 +408,13 @@ export default class SessionWrapper { } static getJWKS(userContext?: Record) { - return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ + return JWTRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ userContext: getUserContext(userContext), }); } static getOpenIdDiscoveryConfiguration(userContext?: Record) { - return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ + return OpenIdRecipe.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext: getUserContext(userContext), }); } diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index 87b3d6bf3..7dd003e3a 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -40,9 +40,9 @@ import APIImplementation from "./api/implementation"; import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { APIOptions } from "."; -import OpenIdRecipe from "../openid/recipe"; import { logDebugMessage } from "../../logger"; -import { isTestEnv } from "../../utils"; +import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; +import { hasGreaterThanEqualToFDI, isTestEnv } from "../../utils"; // For Express export default class SessionRecipe extends RecipeModule { @@ -55,7 +55,6 @@ export default class SessionRecipe extends RecipeModule { config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; - openIdRecipe: OpenIdRecipe; apiImpl: APIInterface; @@ -81,10 +80,6 @@ export default class SessionRecipe extends RecipeModule { this.isInServerlessEnv = isInServerlessEnv; - this.openIdRecipe = new OpenIdRecipe(recipeId, appInfo, isInServerlessEnv, { - override: this.config.override.openIdFeature, - }); - let builder = new OverrideableBuilder( RecipeImplementation( Querier.getNewInstanceOrThrowError(recipeId), @@ -126,6 +121,7 @@ export default class SessionRecipe extends RecipeModule { throw new Error("calling testing function in non testing env"); } SessionRecipe.instance = undefined; + resetCombinedJWKS(); } addClaimFromOtherRecipe = (claim: SessionClaim) => { @@ -168,18 +164,16 @@ export default class SessionRecipe extends RecipeModule { }, ]; - apisHandled.push(...this.openIdRecipe.getAPIsHandled()); - return apisHandled; }; handleAPIRequest = async ( id: string, - tenantId: string, + _tenantId: string, req: BaseRequest, res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod, + _path: NormalisedURLPath, + _method: HTTPMethod, userContext: UserContext ): Promise => { let options: APIOptions = { @@ -195,7 +189,7 @@ export default class SessionRecipe extends RecipeModule { } else if (id === SIGNOUT_API_PATH) { return await signOutAPI(this.apiImpl, options, userContext); } else { - return await this.openIdRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); + return false; } }; @@ -245,23 +239,18 @@ export default class SessionRecipe extends RecipeModule { throw err; } } else { - return await this.openIdRecipe.handleError(err, request, response, userContext); + throw err; } }; getAllCORSHeaders = (): string[] => { let corsHeaders: string[] = [...getCORSAllowedHeadersFromCookiesAndHeaders()]; - corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()); - return corsHeaders; }; isErrorFromThisRecipe = (err: any): err is STError => { - return ( - STError.isErrorFromSuperTokens(err) && - (err.fromRecipe === SessionRecipe.RECIPE_ID || this.openIdRecipe.isErrorFromThisRecipe(err)) - ); + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === SessionRecipe.RECIPE_ID; }; verifySession = async ( @@ -283,4 +272,11 @@ export default class SessionRecipe extends RecipeModule { userContext, }); }; + + getNormalisedOverwriteSessionDuringSignInUp = (req: any) => { + const supportsFDI31 = hasGreaterThanEqualToFDI(req, "3.1"); + const res = this.config.overwriteSessionDuringSignInUp ?? supportsFDI31; + logDebugMessage("getNormalisedOverwriteSessionDuringSignInUp returning: " + res); + return res; + }; } diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts index 437c7ddc4..359c843ee 100644 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ b/lib/ts/recipe/session/recipeImplementation.ts @@ -1,4 +1,3 @@ -import { createRemoteJWKSet, JWTVerifyGetKey } from "jose"; import { RecipeInterface, VerifySessionOptions, @@ -22,12 +21,11 @@ import { validateAccessTokenStructure } from "./accessToken"; import SessionError from "./error"; import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; -import { JWKCacheCooldownInMs, protectedProps } from "./constants"; +import { protectedProps } from "./constants"; import { isTestEnv } from "../../utils"; export type Helpers = { querier: Querier; - JWKS: JWTVerifyGetKey; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; getRecipeImpl: () => RecipeInterface; @@ -39,40 +37,6 @@ export default function getRecipeInterface( appInfo: NormalisedAppinfo, getRecipeImplAfterOverrides: () => RecipeInterface ): RecipeInterface { - const JWKS: ReturnType[] = querier - .getAllCoreUrlsForPath("/.well-known/jwks.json") - .map((url) => - createRemoteJWKSet(new URL(url), { - cooldownDuration: JWKCacheCooldownInMs, - cacheMaxAge: config.jwksRefreshIntervalSec * 1000, - }) - ); - - /** - This function fetches all JWKs from the first available core instance. This combines the other JWKS functions to become - error resistant. - - Every core instance a backend is connected to is expected to connect to the same database and use the same key set for - token verification. Otherwise, the result of session verification would depend on which core is currently available. - */ - const combinedJWKS: ReturnType = async (...args) => { - let lastError = undefined; - if (JWKS.length === 0) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - for (const jwks of JWKS) { - try { - // We await before returning to make sure we catch the error - return await jwks(...args); - } catch (ex) { - lastError = ex; - } - } - throw lastError; - }; - let obj: RecipeInterface = { createNewSession: async function ({ recipeUserId, @@ -602,7 +566,6 @@ export default function getRecipeInterface( let helpers: Helpers = { querier, - JWKS: combinedJWKS, config, appInfo, getRecipeImpl: getRecipeImplAfterOverrides, diff --git a/lib/ts/recipe/session/sessionFunctions.ts b/lib/ts/recipe/session/sessionFunctions.ts index f02c4030e..9c0b1bc40 100644 --- a/lib/ts/recipe/session/sessionFunctions.ts +++ b/lib/ts/recipe/session/sessionFunctions.ts @@ -24,6 +24,7 @@ import { logDebugMessage } from "../../logger"; import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { UserContext } from "../../types"; +import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; /** * @description call this to "login" a user. @@ -111,7 +112,7 @@ export async function getSession( */ accessTokenInfo = await getInfoFromAccessToken( parsedAccessToken, - helpers.JWKS, + getCombinedJWKS(config), helpers.config.antiCsrfFunctionOrString === "VIA_TOKEN" && doAntiCsrfCheck ); } catch (err) { @@ -500,6 +501,7 @@ export async function updateSessionDataInDatabase( sessionHandle, userDataInDatabase: newSessionData, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { @@ -522,6 +524,7 @@ export async function updateAccessTokenPayload( sessionHandle, userDataInJWT: newAccessTokenPayload, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { diff --git a/lib/ts/recipe/session/sessionRequestFunctions.ts b/lib/ts/recipe/session/sessionRequestFunctions.ts index de4c5b3bb..cde8c6744 100644 --- a/lib/ts/recipe/session/sessionRequestFunctions.ts +++ b/lib/ts/recipe/session/sessionRequestFunctions.ts @@ -8,6 +8,7 @@ import { } from "./types"; import frameworks from "../../framework"; import SuperTokens from "../../supertokens"; +import OpenIdRecipe from "../openid/recipe"; import { getRequiredClaimValidators } from "./utils"; import { getRidFromHeader, isAnIpAddress, normaliseHttpMethod, setRequestInUserContextIfNotDefined } from "../../utils"; import { logDebugMessage } from "../../logger"; @@ -73,65 +74,13 @@ export async function getSessionFromRequest({ const sessionOptional = options?.sessionRequired === false; logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens: { - [key in TokenTransferMethod]?: ParsedJWTInfo; - } = {}; - - // We check all token transfer methods for available access tokens - for (const transferMethod of availableTokenTransferMethods) { - const tokenString = getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = parseJWTWithoutSignatureVerification(tokenString); - validateAccessTokenStructure(info.payload, info.version); - logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch { - logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } - const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); - let requestTransferMethod: TokenTransferMethod | undefined; - let accessToken: ParsedJWTInfo | undefined; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logDebugMessage("getSession: using cookie transfer method"); - - // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. - // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). - // ensuring outdated token payload isn't used. - const hasMultipleAccessTokenCookies = hasMultipleCookiesForTokenType(req, "access"); - if (hasMultipleAccessTokenCookies) { - logDebugMessage( - "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" - ); - throw new SessionError({ - message: "Multiple access tokens present in the request cookies.", - type: SessionError.TRY_REFRESH_TOKEN, - }); - } - - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } + const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(req, allowedTransferMethod); let antiCsrfToken = getAntiCsrfTokenFromHeaders(req); let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; @@ -212,6 +161,64 @@ export async function getSessionFromRequest({ return session; } +export function getAccessTokenFromRequest(req: any, allowedTransferMethod: TokenTransferMethod | "any") { + const accessTokens: { + [key in TokenTransferMethod]?: ParsedJWTInfo; + } = {}; + + // We check all token transfer methods for available access tokens + for (const transferMethod of availableTokenTransferMethods) { + const tokenString = getToken(req, "access", transferMethod); + if (tokenString !== undefined) { + try { + const info = parseJWTWithoutSignatureVerification(tokenString); + validateAccessTokenStructure(info.payload, info.version); + logDebugMessage("getSession: got access token from " + transferMethod); + accessTokens[transferMethod] = info; + } catch { + logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` + ); + } + } + } + + let requestTransferMethod: TokenTransferMethod | undefined; + let accessToken: ParsedJWTInfo | undefined; + + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + accessTokens["header"] !== undefined + ) { + logDebugMessage("getSession: using header transfer method"); + requestTransferMethod = "header"; + accessToken = accessTokens["header"]; + } else if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && + accessTokens["cookie"] !== undefined + ) { + logDebugMessage("getSession: using cookie transfer method"); + + // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. + // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). + // ensuring outdated token payload isn't used. + const hasMultipleAccessTokenCookies = hasMultipleCookiesForTokenType(req, "access"); + if (hasMultipleAccessTokenCookies) { + logDebugMessage( + "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" + ); + throw new SessionError({ + message: "Multiple access tokens present in the request cookies.", + type: SessionError.TRY_REFRESH_TOKEN, + }); + } + + requestTransferMethod = "cookie"; + accessToken = accessTokens["cookie"]; + } + return { requestTransferMethod, accessToken, allowedTransferMethod }; +} + /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours @@ -438,7 +445,7 @@ export async function createNewSessionInRequest({ userContext = setRequestInUserContextIfNotDefined(userContext, req); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + const issuer = await OpenIdRecipe.getIssuer(userContext); let finalAccessTokenPayload = { ...accessTokenPayload, diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 0510b8820..accd318bc 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -14,9 +14,7 @@ */ import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; -import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; @@ -88,26 +86,6 @@ export type TypeInput = { builder?: OverrideableBuilder ) => RecipeInterface; apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; }; }; @@ -124,7 +102,7 @@ export type TypeNormalisedInput = { cookieSecure: boolean; sessionExpiredStatusCode: number; errorHandlers: NormalisedErrorHandlers; - overwriteSessionDuringSignInUp: boolean; + overwriteSessionDuringSignInUp: boolean | undefined; antiCsrfFunctionOrString: | "VIA_TOKEN" @@ -147,26 +125,6 @@ export type TypeNormalisedInput = { builder?: OverrideableBuilder ) => RecipeInterface; apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; }; }; diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts index f43570f77..8a2538c49 100644 --- a/lib/ts/recipe/session/utils.ts +++ b/lib/ts/recipe/session/utils.ts @@ -303,7 +303,7 @@ export function validateAndNormaliseUserInput( antiCsrfFunctionOrString: antiCsrf, override, invalidClaimStatusCode, - overwriteSessionDuringSignInUp: config?.overwriteSessionDuringSignInUp ?? false, + overwriteSessionDuringSignInUp: config?.overwriteSessionDuringSignInUp, jwksRefreshIntervalSec: config?.jwksRefreshIntervalSec ?? 3600 * 4, }; } diff --git a/lib/ts/recipe/thirdparty/api/implementation.ts b/lib/ts/recipe/thirdparty/api/implementation.ts index 8bd4265d3..1b633a3e6 100644 --- a/lib/ts/recipe/thirdparty/api/implementation.ts +++ b/lib/ts/recipe/thirdparty/api/implementation.ts @@ -138,6 +138,7 @@ export default function getAPIInterface(): APIInterface { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -160,6 +161,7 @@ export default function getAPIInterface(): APIInterface { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId, userContext, }); diff --git a/lib/ts/recipe/thirdparty/api/signinup.ts b/lib/ts/recipe/thirdparty/api/signinup.ts index f50813fe8..cc57dff1d 100644 --- a/lib/ts/recipe/thirdparty/api/signinup.ts +++ b/lib/ts/recipe/thirdparty/api/signinup.ts @@ -14,10 +14,14 @@ */ import STError from "../error"; -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { APIInterface, APIOptions } from "../"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signInUpAPI( apiImplementation: APIInterface, @@ -82,13 +86,12 @@ export default async function signInUpAPI( const provider = providerResponse; - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, bodyParams); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -102,6 +105,7 @@ export default async function signInUpAPI( oAuthTokens, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts index 2dc3566f0..697bf96aa 100644 --- a/lib/ts/recipe/thirdparty/index.ts +++ b/lib/ts/recipe/thirdparty/index.ts @@ -136,6 +136,7 @@ export default class Wrapper { tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, isVerified, session, + shouldTryLinkingWithSessionUser: !!session, userContext: getUserContext(userContext), }); } diff --git a/lib/ts/recipe/thirdparty/providers/bitbucket.ts b/lib/ts/recipe/thirdparty/providers/bitbucket.ts index 56cc8073f..6d5ace5e5 100644 --- a/lib/ts/recipe/thirdparty/providers/bitbucket.ts +++ b/lib/ts/recipe/thirdparty/providers/bitbucket.ts @@ -14,7 +14,7 @@ */ import { ProviderInput, TypeProvider } from "../types"; -import { doGetRequest } from "./utils"; +import { doGetRequest } from "../../../thirdpartyUtils"; import NewProvider from "./custom"; import { logDebugMessage } from "../../../logger"; diff --git a/lib/ts/recipe/thirdparty/providers/custom.ts b/lib/ts/recipe/thirdparty/providers/custom.ts index 6e24fc0b6..ba7226836 100644 --- a/lib/ts/recipe/thirdparty/providers/custom.ts +++ b/lib/ts/recipe/thirdparty/providers/custom.ts @@ -1,5 +1,5 @@ import { TypeProvider, ProviderInput, UserInfo, ProviderConfigForClientType } from "../types"; -import { doGetRequest, doPostRequest, verifyIdTokenFromJWKSEndpointAndGetPayload } from "./utils"; +import { doGetRequest, doPostRequest, verifyIdTokenFromJWKSEndpointAndGetPayload } from "../../../thirdpartyUtils"; import pkceChallenge from "pkce-challenge"; import { getProviderConfigForClient } from "./configUtils"; import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; diff --git a/lib/ts/recipe/thirdparty/providers/github.ts b/lib/ts/recipe/thirdparty/providers/github.ts index 5a62474fd..445cc992f 100644 --- a/lib/ts/recipe/thirdparty/providers/github.ts +++ b/lib/ts/recipe/thirdparty/providers/github.ts @@ -15,7 +15,7 @@ import { encodeBase64 } from "../../../utils"; import { ProviderInput, TypeProvider, UserInfo } from "../types"; import NewProvider from "./custom"; -import { doGetRequest, doPostRequest } from "./utils"; +import { doGetRequest, doPostRequest } from "../../../thirdpartyUtils"; function getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoResponse: { fromIdTokenPayload?: any; diff --git a/lib/ts/recipe/thirdparty/providers/linkedin.ts b/lib/ts/recipe/thirdparty/providers/linkedin.ts index 5aa79976f..c179b0269 100644 --- a/lib/ts/recipe/thirdparty/providers/linkedin.ts +++ b/lib/ts/recipe/thirdparty/providers/linkedin.ts @@ -15,7 +15,7 @@ import { logDebugMessage } from "../../../logger"; import { ProviderInput, TypeProvider } from "../types"; import NewProvider from "./custom"; -import { doGetRequest } from "./utils"; +import { doGetRequest } from "../../../thirdpartyUtils"; export default function Linkedin(input: ProviderInput): TypeProvider { if (input.config.name === undefined) { diff --git a/lib/ts/recipe/thirdparty/providers/twitter.ts b/lib/ts/recipe/thirdparty/providers/twitter.ts index e6474465d..fec417766 100644 --- a/lib/ts/recipe/thirdparty/providers/twitter.ts +++ b/lib/ts/recipe/thirdparty/providers/twitter.ts @@ -20,7 +20,7 @@ import NewProvider, { getActualClientIdFromDevelopmentClientId, isUsingDevelopmentClientId, } from "./custom"; -import { doPostRequest } from "./utils"; +import { doPostRequest } from "../../../thirdpartyUtils"; export default function Twitter(input: ProviderInput): TypeProvider { if (input.config.name === undefined) { diff --git a/lib/ts/recipe/thirdparty/providers/utils.ts b/lib/ts/recipe/thirdparty/providers/utils.ts index da1a8f895..56544f48f 100644 --- a/lib/ts/recipe/thirdparty/providers/utils.ts +++ b/lib/ts/recipe/thirdparty/providers/utils.ts @@ -1,6 +1,6 @@ import * as jose from "jose"; - import { ProviderConfigForClientType } from "../types"; +import { getOIDCDiscoveryInfo } from "../../../thirdpartyUtils"; import NormalisedURLDomain from "../../../normalisedURLDomain"; import NormalisedURLPath from "../../../normalisedURLPath"; import { logDebugMessage } from "../../../logger"; @@ -99,27 +99,6 @@ export async function verifyIdTokenFromJWKSEndpointAndGetPayload( return payload; } -// OIDC utils -var oidcInfoMap: { [key: string]: any } = {}; - -async function getOIDCDiscoveryInfo(issuer: string): Promise { - if (oidcInfoMap[issuer] !== undefined) { - return oidcInfoMap[issuer]; - } - - const normalizedDomain = new NormalisedURLDomain(issuer); - const normalizedPath = new NormalisedURLPath(issuer); - - let oidcInfo = await doGetRequest(normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous()); - if (oidcInfo.status > 400) { - logDebugMessage(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); - throw new Error(`Received response with status ${oidcInfo!.status} and body ${oidcInfo!.stringResponse}`); - } - - oidcInfoMap[issuer] = oidcInfo.jsonResponse!; - return oidcInfo.jsonResponse!; -} - export async function discoverOIDCEndpoints(config: ProviderConfigForClientType): Promise { if (config.oidcDiscoveryEndpoint !== undefined) { const oidcInfo = await getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts index f9a602e3c..6225009bd 100644 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ b/lib/ts/recipe/thirdparty/recipeImplementation.ts @@ -15,7 +15,16 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro return { manuallyCreateOrUpdateUser: async function ( this: RecipeInterface, - { thirdPartyId, thirdPartyUserId, email, isVerified, tenantId, session, userContext } + { + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + tenantId, + session, + shouldTryLinkingWithSessionUser, + userContext, + } ) { const accountLinking = AccountLinking.getInstance(); const users = await listUsersByAccountInfo( @@ -71,8 +80,9 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro // function updated the verification status) and can return that response.user = (await getUser(response.recipeUserId.getAsString(), userContext))!; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, + shouldTryLinkingWithSessionUser, inputUser: response.user, recipeUserId: response.recipeUserId, session, @@ -102,6 +112,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro userContext, oAuthTokens, session, + shouldTryLinkingWithSessionUser, rawUserInfoFromProvider, } ): Promise< @@ -136,6 +147,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro tenantId, isVerified, session, + shouldTryLinkingWithSessionUser, userContext, }); diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index 21c7608da..71e9847d3 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -174,6 +174,7 @@ export type RecipeInterface = { fromUserInfoAPI?: { [key: string]: any }; }; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -208,6 +209,7 @@ export type RecipeInterface = { email: string; isVerified: boolean; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -272,6 +274,7 @@ export type APIInterface = { provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/ts/recipe/totp/recipeImplementation.ts b/lib/ts/recipe/totp/recipeImplementation.ts index 745f23cf2..a87a35c39 100644 --- a/lib/ts/recipe/totp/recipeImplementation.ts +++ b/lib/ts/recipe/totp/recipeImplementation.ts @@ -123,6 +123,7 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, }, + {}, input.userContext ); }, diff --git a/lib/ts/recipe/usermetadata/recipeImplementation.ts b/lib/ts/recipe/usermetadata/recipeImplementation.ts index 51ab77ea5..2ab9d24e2 100644 --- a/lib/ts/recipe/usermetadata/recipeImplementation.ts +++ b/lib/ts/recipe/usermetadata/recipeImplementation.ts @@ -30,6 +30,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { userId, metadataUpdate, }, + {}, userContext ); }, diff --git a/lib/ts/recipe/userroles/recipe.ts b/lib/ts/recipe/userroles/recipe.ts index 38c5a3e5b..be699c436 100644 --- a/lib/ts/recipe/userroles/recipe.ts +++ b/lib/ts/recipe/userroles/recipe.ts @@ -19,7 +19,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import RecipeImplementation from "./recipeImplementation"; import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; @@ -27,8 +27,11 @@ import { validateAndNormaliseUserInput } from "./utils"; import OverrideableBuilder from "supertokens-js-override"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; import SessionRecipe from "../session/recipe"; +import OAuth2Recipe from "../oauth2provider/recipe"; import { UserRoleClaim } from "./userRoleClaim"; import { PermissionClaim } from "./permissionClaim"; +import { User } from "../../user"; +import { getSessionInformation } from "../session"; import { isTestEnv } from "../../utils"; export default class Recipe extends RecipeModule { @@ -56,6 +59,114 @@ export default class Recipe extends RecipeModule { if (!this.config.skipAddingPermissionsToAccessToken) { SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(PermissionClaim); } + + const tokenPayloadBuilder = async ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ) => { + let payload: { + roles?: string[]; + permissions?: string[]; + } = {}; + + const sessionInfo = await getSessionInformation(sessionHandle, userContext); + + let userRoles: string[] = []; + + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId: sessionInfo!.tenantId, + userContext, + }); + + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + + if (scopes.includes("roles")) { + payload.roles = userRoles; + } + + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + + payload.permissions = Array.from(userPermissions); + } + + return payload; + }; + + OAuth2Recipe.getInstanceOrThrowError().addAccessTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + OAuth2Recipe.getInstanceOrThrowError().addIdTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + + OAuth2Recipe.getInstanceOrThrowError().addUserInfoBuilderFromOtherRecipe( + async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo: { + roles?: string[]; + permissions?: string[]; + } = {}; + + let userRoles: string[] = []; + + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + + if (scopes.includes("roles")) { + userInfo.roles = userRoles; + } + + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + + userInfo.permissions = Array.from(userPermissions); + } + + return userInfo; + } + ); }); } diff --git a/lib/ts/recipe/userroles/recipeImplementation.ts b/lib/ts/recipe/userroles/recipeImplementation.ts index eaee083f7..7141a8bfe 100644 --- a/lib/ts/recipe/userroles/recipeImplementation.ts +++ b/lib/ts/recipe/userroles/recipeImplementation.ts @@ -24,6 +24,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { return querier.sendPutRequest( new NormalisedURLPath(`/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user/role`), { userId, role }, + {}, userContext ); }, @@ -55,7 +56,12 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { }, createNewRoleOrAddPermissions: function ({ role, permissions, userContext }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/role"), { role, permissions }, userContext); + return querier.sendPutRequest( + new NormalisedURLPath("/recipe/role"), + { role, permissions }, + {}, + userContext + ); }, getPermissionsForRole: function ({ role, userContext }) { diff --git a/lib/ts/recipeModule.ts b/lib/ts/recipeModule.ts index 3b73692c8..1f3c14b45 100644 --- a/lib/ts/recipeModule.ts +++ b/lib/ts/recipeModule.ts @@ -22,7 +22,7 @@ import { DEFAULT_TENANT_ID } from "./recipe/multitenancy/constants"; export default abstract class RecipeModule { private recipeId: string; - private appInfo: NormalisedAppinfo; + protected appInfo: NormalisedAppinfo; constructor(recipeId: string, appInfo: NormalisedAppinfo) { this.recipeId = recipeId; @@ -41,7 +41,7 @@ export default abstract class RecipeModule { path: NormalisedURLPath, method: HTTPMethod, userContext: UserContext - ): Promise<{ id: string; tenantId: string } | undefined> => { + ): Promise<{ id: string; tenantId: string; exactMatch: boolean } | undefined> => { let apisHandled = this.getAPIsHandled(); const basePathStr = this.appInfo.apiBasePath.getAsStringDangerous(); @@ -71,7 +71,7 @@ export default abstract class RecipeModule { tenantIdFromFrontend: DEFAULT_TENANT_ID, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: true }; } else if ( remainingPath !== undefined && this.appInfo.apiBasePath @@ -82,7 +82,7 @@ export default abstract class RecipeModule { tenantIdFromFrontend: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: false }; } } } diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts index 174fcdb4a..145266186 100644 --- a/lib/ts/supertokens.ts +++ b/lib/ts/supertokens.ts @@ -104,6 +104,9 @@ export default class SuperTokens { let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; + let oauth2Found = false; + let openIdFound = false; + let jwtFound = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way // so that there is no circular dependency. Otherwise there would be cyclic dependency @@ -112,6 +115,9 @@ export default class SuperTokens { let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; let TotpRecipe = require("./recipe/totp/recipe").default; + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; + let OpenIdRecipe = require("./recipe/openid/recipe").default; + let jwtRecipe = require("./recipe/jwt/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); @@ -123,10 +129,22 @@ export default class SuperTokens { multiFactorAuthFound = true; } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { totpFound = true; + } else if (recipeModule.getRecipeId() === OAuth2ProviderRecipe.RECIPE_ID) { + oauth2Found = true; + } else if (recipeModule.getRecipeId() === OpenIdRecipe.RECIPE_ID) { + openIdFound = true; + } else if (recipeModule.getRecipeId() === jwtRecipe.RECIPE_ID) { + jwtFound = true; } return recipeModule; }); + if (!jwtFound) { + this.recipeModules.push(jwtRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } + if (!openIdFound) { + this.recipeModules.push(OpenIdRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } if (!multitenancyFound) { this.recipeModules.push(MultitenancyRecipe.init()(this.appInfo, this.isInServerlessEnv)); } @@ -143,6 +161,10 @@ export default class SuperTokens { // To let those cases function without initializing account linking we do not check it here, but when // the authentication endpoints are called. + // We've decided to always initialize the OAuth2Provider recipe + if (!oauth2Found) { + this.recipeModules.push(OAuth2ProviderRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } this.telemetryEnabled = config.telemetry === undefined ? !isTestEnv() : config.telemetry; } @@ -157,6 +179,17 @@ export default class SuperTokens { if (!isTestEnv()) { throw new Error("calling testing function in non testing env"); } + + // We call reset the following recipes because they are auto-initialized + // and there is no case where we want to reset the SuperTokens instance but not + // the recipes. + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; + OAuth2ProviderRecipe.reset(); + let OpenIdRecipe = require("./recipe/openid/recipe").default; + OpenIdRecipe.reset(); + let JWTRecipe = require("./recipe/jwt/recipe").default; + JWTRecipe.reset(); + Querier.reset(); SuperTokens.instance = undefined; } @@ -334,6 +367,7 @@ export default class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, + {}, input.userContext ); } else { @@ -363,6 +397,13 @@ export default class SuperTokens { } async function handleWithoutRid(recipeModules: RecipeModule[]) { + let bestMatch: + | { + recipeModule: RecipeModule; + idResult: { id: string; tenantId: string; exactMatch: boolean }; + } + | undefined = undefined; + for (let i = 0; i < recipeModules.length; i++) { logDebugMessage( "middleware: Checking recipe ID for match: " + @@ -374,24 +415,40 @@ export default class SuperTokens { ); let idResult = await recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (idResult !== undefined) { - logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); - let requestHandled = await recipeModules[i].handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext - ); - if (!requestHandled) { - logDebugMessage("middleware: Not handled because API returned requestHandled as false"); - return false; + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + if (bestMatch === undefined || idResult.exactMatch) { + bestMatch = { + recipeModule: recipeModules[i], + idResult: idResult, + }; + } + + if (idResult.exactMatch) { + break; } - logDebugMessage("middleware: Ended"); - return true; } } + + if (bestMatch !== undefined) { + const { idResult, recipeModule } = bestMatch; + logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + let requestHandled = await recipeModule.handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, + path, + method, + userContext + ); + if (!requestHandled) { + logDebugMessage("middleware: Not handled because API returned requestHandled as false"); + return false; + } + logDebugMessage("middleware: Ended"); + return true; + } logDebugMessage("middleware: Not handling because no recipe matched"); return false; } @@ -435,6 +492,7 @@ export default class SuperTokens { | { id: string; tenantId: string; + exactMatch: boolean; } | undefined = undefined; @@ -444,13 +502,18 @@ export default class SuperTokens { // the path and methods of the APIs exposed via those recipes is unique. let currIdResult = await matchedRecipe[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (currIdResult !== undefined) { - if (idResult !== undefined) { + if ( + idResult === undefined || + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + (currIdResult.exactMatch === true && idResult.exactMatch === false) + ) { + finalMatchedRecipe = matchedRecipe[i]; + idResult = currIdResult; + } else { throw new Error( "Two recipes have matched the same API path and method! This is a bug in the SDK. Please contact support." ); - } else { - finalMatchedRecipe = matchedRecipe[i]; - idResult = currIdResult; } } } diff --git a/lib/ts/thirdpartyUtils.ts b/lib/ts/thirdpartyUtils.ts new file mode 100644 index 000000000..54a268888 --- /dev/null +++ b/lib/ts/thirdpartyUtils.ts @@ -0,0 +1,119 @@ +import * as jose from "jose"; +import { logDebugMessage } from "./logger"; +import { doFetch } from "./utils"; +import NormalisedURLDomain from "./normalisedURLDomain"; +import NormalisedURLPath from "./normalisedURLPath"; + +export async function doGetRequest( + url: string, + queryParams?: { [key: string]: string }, + headers?: { [key: string]: string } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}> { + logDebugMessage( + `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` + ); + if (headers?.["Accept"] === undefined) { + headers = { + ...headers, + Accept: "application/json", + }; + } + const finalURL = new URL(url); + finalURL.search = new URLSearchParams(queryParams).toString(); + let response = await doFetch(finalURL.toString(), { + headers: headers, + }); + + const stringResponse = await response.text(); + let jsonResponse: Record | undefined = undefined; + + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + + logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} + +export async function doPostRequest( + url: string, + params: { [key: string]: any }, + headers?: { [key: string]: string } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}> { + if (headers === undefined) { + headers = {}; + } + + headers["Content-Type"] = "application/x-www-form-urlencoded"; + headers["Accept"] = "application/json"; + + logDebugMessage( + `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` + ); + + const body = new URLSearchParams(params).toString(); + let response = await doFetch(url, { + method: "POST", + body, + headers, + }); + + const stringResponse = await response.text(); + let jsonResponse: Record | undefined = undefined; + + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + + logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} + +export async function verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken: string, + jwks: jose.JWTVerifyGetKey, + otherOptions: jose.JWTVerifyOptions +): Promise { + const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); + + return payload; +} + +// OIDC utils +var oidcInfoMap: { [key: string]: any } = {}; + +export async function getOIDCDiscoveryInfo(issuer: string): Promise { + const normalizedDomain = new NormalisedURLDomain(issuer); + let normalizedPath = new NormalisedURLPath(issuer); + + if (oidcInfoMap[issuer] !== undefined) { + return oidcInfoMap[issuer]; + } + const oidcInfo = await doGetRequest( + normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() + ); + + if (oidcInfo.status >= 400) { + logDebugMessage(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + } + + oidcInfoMap[issuer] = oidcInfo.jsonResponse!; + return oidcInfo.jsonResponse!; +} diff --git a/lib/ts/types.ts b/lib/ts/types.ts index a90d96f0f..e87e6b2a7 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -24,6 +24,11 @@ type Brand = { [__brand]: B }; type Branded = T & Brand; +// A utility type that makes all properties of a given type non-nullable. +export type NonNullableProperties = { + [P in keyof T]: NonNullable; +}; + // Record is still quite generic and we would like to ensure type safety for the userContext // so we use the concept of branded type, which enables catching of issues at compile time. // Detailed explanation about branded types is available here - https://egghead.io/blog/using-branded-types-in-typescript @@ -86,7 +91,7 @@ export type APIHandled = { disabled: boolean; }; -export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +export type HTTPMethod = "post" | "get" | "delete" | "put" | "patch" | "options" | "trace"; export type JSONPrimitive = string | number | boolean | null; export type JSONArray = Array; diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index a19cba132..c59fee395 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -18,11 +18,13 @@ export const doFetch: typeof fetch = async (input: RequestInfo | URL, init?: Req ProcessState.getInstance().addState(PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH); init = { cache: "no-cache", + redirect: "manual", }; } else { if (init.cache === undefined) { ProcessState.getInstance().addState(PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH); init.cache = "no-cache"; + init.redirect = "manual"; } } const fetchFunction = typeof fetch !== "undefined" ? fetch : crossFetch; @@ -192,6 +194,12 @@ export function isAnIpAddress(ipaddress: string) { ipaddress ); } +export function getNormalisedShouldTryLinkingWithSessionUserFlag(req: BaseRequest, body: any) { + if (hasGreaterThanEqualToFDI(req, "3.1")) { + return body.shouldTryLinkingWithSessionUser ?? false; + } + return undefined; +} export function getBackwardsCompatibleUserInfo( req: BaseRequest, @@ -433,6 +441,27 @@ export function normaliseEmail(email: string): string { return email; } +export function toCamelCase(str: string): string { + return str.replace(/([-_][a-z])/gi, (match) => { + return match.toUpperCase().replace("-", "").replace("_", ""); + }); +} + +export function toSnakeCase(str: string): string { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} + +// Transforms the keys of an object from camelCase to snakeCase or vice versa. +export function transformObjectKeys(obj: { [key: string]: any }, caseType: "snake-case" | "camelCase"): T { + const transformKey = caseType === "camelCase" ? toCamelCase : toSnakeCase; + + return Object.entries(obj).reduce((result, [key, value]) => { + const transformedKey = transformKey(key); + result[transformedKey] = value; + return result; + }, {} as any) as T; +} + export const getProcess = () => { /** * Return the process instance if it is available falling back diff --git a/lib/ts/version.ts b/lib/ts/version.ts index bbaa5c475..a072bf0ae 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,9 +12,9 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "20.1.3"; +export const version = "21.0.0"; -export const cdiSupported = ["5.1"]; +export const cdiSupported = ["5.2"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} export const dashboardVersion = "0.13"; diff --git a/package-lock.json b/package-lock.json index 2b3538590..1b76be6c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supertokens-node", - "version": "20.1.3", - "lockfileVersion": 2, + "version": "21.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supertokens-node", - "version": "20.1.3", + "version": "21.0.0", "license": "Apache-2.0", "dependencies": { "buffer": "^6.0.3", @@ -20,6 +20,7 @@ "pako": "^2.1.0", "pkce-challenge": "^3.0.0", "process": "^0.11.10", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "tldts": "^6.1.48", "twilio": "^4.19.3" @@ -42,7 +43,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/pako": "^2.0.3", - "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", @@ -1182,70 +1183,6 @@ "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==", "dev": true }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", @@ -1278,54 +1215,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@openapi-contrib/openapi-schema-to-json-schema": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.3.2.tgz", @@ -1694,12 +1583,6 @@ "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", "dev": true }, - "node_modules/@types/psl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", - "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", - "dev": true - }, "node_modules/@types/qs": { "version": "6.9.14", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", @@ -1732,6 +1615,15 @@ "@types/node": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/type-is": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", @@ -3760,20 +3652,6 @@ "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, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7070,8 +6948,7 @@ "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -8257,6415 +8134,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dev": true, - "requires": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" - } - }, - "@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", - "dev": true - }, - "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "dependencies": { - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", - "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", - "dev": true, - "requires": { - "@babel/types": "^7.24.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", - "dev": true, - "requires": { - "@babel/types": "^7.24.0" - } - }, - "@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", - "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", - "dev": true, - "requires": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" - } - }, - "@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", - "dev": true - }, - "@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - } - }, - "@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@extra-number/significant-digits": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@extra-number/significant-digits/-/significant-digits-1.3.9.tgz", - "integrity": "sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==", - "dev": true - }, - "@fastify/ajv-compiler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", - "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", - "dev": true, - "requires": { - "ajv": "^6.12.6" - } - }, - "@hapi/accept": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-5.0.2.tgz", - "integrity": "sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/ammo": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-5.0.1.tgz", - "integrity": "sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/b64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-5.0.0.tgz", - "integrity": "sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/bounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-2.0.0.tgz", - "integrity": "sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/bourne": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz", - "integrity": "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q==", - "dev": true - }, - "@hapi/call": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@hapi/call/-/call-8.0.1.tgz", - "integrity": "sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/catbox": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-11.1.1.tgz", - "integrity": "sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/podium": "4.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/catbox-memory": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz", - "integrity": "sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/content": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/content/-/content-5.0.2.tgz", - "integrity": "sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x" - } - }, - "@hapi/cryptiles": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", - "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x" - } - }, - "@hapi/file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/file/-/file-2.0.0.tgz", - "integrity": "sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==", - "dev": true - }, - "@hapi/hapi": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-20.3.0.tgz", - "integrity": "sha512-zvPSRvaQyF3S6Nev9aiAzko2/hIFZmNSJNcs07qdVaVAvj8dGJSV4fVUuQSnufYJAGiSau+U5LxMLhx79se5WA==", - "dev": true, - "requires": { - "@hapi/accept": "^5.0.1", - "@hapi/ammo": "^5.0.1", - "@hapi/boom": "^9.1.0", - "@hapi/bounce": "^2.0.0", - "@hapi/call": "^8.0.0", - "@hapi/catbox": "^11.1.1", - "@hapi/catbox-memory": "^5.0.0", - "@hapi/heavy": "^7.0.1", - "@hapi/hoek": "^9.0.4", - "@hapi/mimos": "^6.0.0", - "@hapi/podium": "^4.1.1", - "@hapi/shot": "^5.0.5", - "@hapi/somever": "^3.0.0", - "@hapi/statehood": "^7.0.3", - "@hapi/subtext": "^7.1.0", - "@hapi/teamwork": "^5.1.0", - "@hapi/topo": "^5.0.0", - "@hapi/validate": "^1.1.1" - } - }, - "@hapi/heavy": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-7.0.1.tgz", - "integrity": "sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "@hapi/iron": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-6.0.0.tgz", - "integrity": "sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==", - "dev": true, - "requires": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/mimos": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-6.0.0.tgz", - "integrity": "sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "mime-db": "1.x.x" - } - }, - "@hapi/nigel": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-4.0.2.tgz", - "integrity": "sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.4", - "@hapi/vise": "^4.0.0" - } - }, - "@hapi/pez": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-5.1.0.tgz", - "integrity": "sha512-YfB0btnkLB3lb6Ry/1KifnMPBm5ZPfaAHWFskzOMAgDgXgcBgA+zjpIynyEiBfWEz22DBT8o1e2tAaBdlt8zbw==", - "dev": true, - "requires": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/content": "^5.0.2", - "@hapi/hoek": "9.x.x", - "@hapi/nigel": "4.x.x" - } - }, - "@hapi/podium": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-4.1.3.tgz", - "integrity": "sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "@hapi/teamwork": "5.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/shot": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-5.0.5.tgz", - "integrity": "sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/somever": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-3.0.1.tgz", - "integrity": "sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==", - "dev": true, - "requires": { - "@hapi/bounce": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/statehood": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-7.0.4.tgz", - "integrity": "sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bounce": "2.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/iron": "6.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/subtext": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-7.1.0.tgz", - "integrity": "sha512-n94cU6hlvsNRIpXaROzBNEJGwxC+HA69q769pChzej84On8vsU14guHDub7Pphr/pqn5b93zV3IkMPDU5AUiXA==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/content": "^5.0.2", - "@hapi/file": "2.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/pez": "^5.1.0", - "@hapi/wreck": "17.x.x" - } - }, - "@hapi/teamwork": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-5.1.1.tgz", - "integrity": "sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg==", - "dev": true - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@hapi/validate": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-1.1.3.tgz", - "integrity": "sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0" - } - }, - "@hapi/vise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-4.0.0.tgz", - "integrity": "sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/wreck": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-17.2.0.tgz", - "integrity": "sha512-pJ5kjYoRPYDv+eIuiLQqhGon341fr2bNIYZjuotuPJG/3Ilzr/XtI+JAp0A86E2bYfsS3zBPABuS2ICkaXFT8g==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@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, - "requires": { - "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" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - } - } - }, - "@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 - }, - "@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, - "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "@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 - }, - "@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 - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@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, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@koa/router": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-10.1.1.tgz", - "integrity": "sha512-ORNjq5z4EmQPriKbR0ER3k4Gh7YGNhWDL7JBW+8wXDrHLbWYKYSJaOJ9aN06npF5tbTxe2JBOsurpJDAvjiXKw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - } - }, - "@loopback/context": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@loopback/context/-/context-3.18.0.tgz", - "integrity": "sha512-PKx0rTguqBj6mUHBbEHLF031MnP6KiSkMLE4E8Hpy2KPJxG97HUT2ZUACHCP6qm8yS9spWQQ6g72VYAWxDrN+g==", - "dev": true, - "requires": { - "@loopback/metadata": "^3.3.4", - "@types/debug": "^4.1.7", - "debug": "^4.3.2", - "hyperid": "^2.3.1", - "p-event": "^4.2.0", - "tslib": "^2.3.1", - "uuid": "^8.3.2" - } - }, - "@loopback/core": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@loopback/core/-/core-2.16.2.tgz", - "integrity": "sha512-KtkNv6HIh8TFBOxTkfPp/BQbVqjDsGef/DtbNHH1ZHs3gSbofhkZs3IqQdYQzpkUq71mQjz5RJ/yUYY5Sqva9w==", - "dev": true, - "requires": { - "@loopback/context": "^3.17.1", - "debug": "^4.3.1", - "tslib": "^2.3.0" - } - }, - "@loopback/filter": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-1.5.4.tgz", - "integrity": "sha512-kfdCgSy0YoAFNYXJOpag5uJnlErYcROIeJqeAglkwOa3hSw2BYIudurU8hoqsiOBIGhI5BF4A3S8u4q089xWlg==", - "dev": true, - "requires": { - "tslib": "^2.3.1" - } - }, - "@loopback/http-server": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-2.5.4.tgz", - "integrity": "sha512-M7w+4AEhwDn7q00soCe8yYQDUS+n87ppuXQ1rJ9a1b9TdnEh+7nPFVrVpwiEKBGyVGIJWDq5BMSZYo1zMIPFUA==", - "dev": true, - "requires": { - "debug": "^4.3.2", - "stoppable": "^1.1.0", - "tslib": "^2.3.1" - } - }, - "@loopback/metadata": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-3.3.4.tgz", - "integrity": "sha512-FISs8OVYKB+wmL0VZdsDZzMOc/KC6anOf3ORpFRO2Mgl9dKCOD8IELKc8r/nr2kyD4r7/pjr5GfLy4nirS1vnQ==", - "dev": true, - "requires": { - "debug": "^4.3.2", - "lodash": "^4.17.21", - "reflect-metadata": "^0.1.13", - "tslib": "^2.3.1" - } - }, - "@loopback/openapi-v3": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-5.3.1.tgz", - "integrity": "sha512-MBVamgxDDbgQQlQjIxSOnaVXLx6plxzn3e8CW8YbNc3TNiS1P8EFa5vNBp8wIzSDTeEd3ic6qzUxCUZIICiFNA==", - "dev": true, - "requires": { - "@loopback/repository-json-schema": "^3.4.1", - "debug": "^4.3.1", - "http-status": "^1.5.0", - "json-merge-patch": "^1.0.1", - "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", - "tslib": "^2.2.0" - }, - "dependencies": { - "@loopback/repository-json-schema": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-3.4.1.tgz", - "integrity": "sha512-E9UKegav+8Bp0MLPQu33c7tWUmWbnKARy0Uu2m7nvP3e3t3WOwB8U9hMjX/wBOhJ4UFJCXAXlq1MulQ/R3dyTw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "debug": "^4.3.1", - "tslib": "^2.2.0" - } - } - } - }, - "@loopback/repository": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-3.7.1.tgz", - "integrity": "sha512-q9vpgQ5MSZqI/ww2TTuDy0Y34NZaapS0+4ZKcwVgwH0XJFxgGwznc5W0l1Esu1lplijejpza4ItKwnlGmvYcJg==", - "dev": true, - "requires": { - "@loopback/filter": "^1.5.2", - "@types/debug": "^4.1.5", - "debug": "^4.3.1", - "lodash": "^4.17.21", - "loopback-datasource-juggler": "^4.26.0", - "tslib": "^2.3.0" - } - }, - "@loopback/rest": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-9.3.0.tgz", - "integrity": "sha512-Qwn5WXctQ2AV6Duze/x7ks9Tb/Oy6FSs0Hhyuhzz7aassKbu1m/GiJLB7bvpJVUN+qVZ2+vQZZ6Y3F+znzH4og==", - "dev": true, - "requires": { - "@loopback/express": "^3.3.0", - "@loopback/http-server": "^2.5.0", - "@loopback/openapi-v3": "^5.3.0", - "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.0", - "@types/body-parser": "^1.19.0", - "@types/cors": "^2.8.10", - "@types/express": "^4.17.11", - "@types/express-serve-static-core": "^4.17.19", - "@types/http-errors": "^1.8.0", - "@types/on-finished": "^2.3.1", - "@types/serve-static": "1.13.9", - "@types/type-is": "^1.6.3", - "ajv": "^6.12.6", - "ajv-errors": "^1.0.1", - "ajv-keywords": "^3.5.2", - "body-parser": "^1.19.0", - "cors": "^2.8.5", - "debug": "^4.3.1", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "js-yaml": "^4.1.0", - "json-schema-compare": "^0.2.2", - "lodash": "^4.17.21", - "on-finished": "^2.3.0", - "path-to-regexp": "^6.2.0", - "qs": "^6.10.1", - "strong-error-handler": "^4.0.0", - "tslib": "^2.2.0", - "type-is": "^1.6.18", - "validator": "^13.6.0" - }, - "dependencies": { - "@loopback/express": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/express/-/express-3.3.4.tgz", - "integrity": "sha512-y+7fu/aXGp7+5QhEnKhwmI/vKLAQtsyvEXTiT8stbj4VHWvNbUbYVwfud5IOeH7d+lhxlNQ5aEJ7IDwoM8xD6Q==", - "dev": true, - "requires": { - "@loopback/http-server": "^2.5.4", - "@types/body-parser": "^1.19.1", - "@types/express": "^4.17.13", - "@types/express-serve-static-core": "^4.17.24", - "@types/http-errors": "^1.8.1", - "body-parser": "^1.19.0", - "debug": "^4.3.2", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "on-finished": "^2.3.0", - "toposort": "^2.0.2", - "tslib": "^2.3.1" - } - }, - "@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - } - } - }, - "@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==", - "dev": true - }, - "@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", - "dev": true, - "optional": true - }, - "@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", - "dev": true, - "optional": true - }, - "@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", - "dev": true, - "optional": true - }, - "@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", - "dev": true, - "optional": true - }, - "@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", - "dev": true, - "optional": true - }, - "@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", - "dev": true, - "optional": true - }, - "@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", - "dev": true, - "optional": true - }, - "@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", - "dev": true, - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", - "dev": true, - "optional": true - }, - "@openapi-contrib/openapi-schema-to-json-schema": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.3.2.tgz", - "integrity": "sha512-aqyc5iEZsUF8qYNxwJNkHYoFxqdoPkqVTnDsj5gqhU+arG4QqLaIDcEOaG0EtKlFBGmSLsQbFYsINiladCJb3g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/samsam": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", - "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, - "@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", - "dev": true, - "requires": { - "tslib": "^2.4.0" - } - }, - "@types/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/aws-lambda": { - "version": "8.10.77", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.77.tgz", - "integrity": "sha512-n0EMFJU/7u3KvHrR83l/zrKOVURXl5pUJPNED/Bzjah89QKCHwCiKCBoVUXRwTGRfCYGIDdinJaAlKDHZdp/Ng==", - "dev": true - }, - "@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/brotli": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz", - "integrity": "sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/co-body": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-5.1.1.tgz", - "integrity": "sha512-0/6AjTfQc5OJUchOS4OHiXNPZVuk+5XvEC2vdcizw/bwx0yb0xY7TKSf8JYvQYZ/OJDiAEjWzxnMjGPnSVlPmA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*" - } - }, - "@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/content-disposition": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", - "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", - "dev": true - }, - "@types/content-type": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", - "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", - "dev": true - }, - "@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", - "dev": true - }, - "@types/cookies": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", - "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, - "@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "requires": { - "@types/ms": "*" - } - }, - "@types/express": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", - "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.43", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", - "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "@types/hapi__catbox": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@types/hapi__catbox/-/hapi__catbox-10.2.6.tgz", - "integrity": "sha512-qdMHk4fBlwRfnBBDJaoaxb+fU9Ewi2xqkXD3mNjSPl2v/G/8IJbDpVRBuIcF7oXrcE8YebU5M8cCeKh1NXEn0w==", - "dev": true - }, - "@types/hapi__hapi": { - "version": "20.0.8", - "resolved": "https://registry.npmjs.org/@types/hapi__hapi/-/hapi__hapi-20.0.8.tgz", - "integrity": "sha512-NNslrYq2XQwm4uOqNcSWKpYtaeMr4DkQdrFzSB7p9rKB9ppJLh3mgP2wak9vBZl7/Cnhhb+JVBcUZCOUcW0JPA==", - "dev": true, - "requires": { - "@hapi/boom": "^9.0.0", - "@hapi/iron": "^6.0.0", - "@hapi/podium": "^4.1.3", - "@types/hapi__catbox": "*", - "@types/hapi__mimos": "*", - "@types/hapi__shot": "*", - "@types/node": "*", - "joi": "^17.3.0" - } - }, - "@types/hapi__mimos": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/hapi__mimos/-/hapi__mimos-4.1.4.tgz", - "integrity": "sha512-i9hvJpFYTT/qzB5xKWvDYaSXrIiNqi4ephi+5Lo6+DoQdwqPXQgmVVOZR+s3MBiHoFqsCZCX9TmVWG3HczmTEQ==", - "dev": true, - "requires": { - "@types/mime-db": "*" - } - }, - "@types/hapi__shot": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/hapi__shot/-/hapi__shot-4.1.6.tgz", - "integrity": "sha512-h33NBjx2WyOs/9JgcFeFhkxnioYWQAZxOHdmqDuoJ1Qjxpcs+JGvSjEEoDeWfcrF+1n47kKgqph5IpfmPOnzbg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/http-assert": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", - "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", - "dev": true - }, - "@types/http-errors": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", - "dev": true - }, - "@types/inflation": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/inflation/-/inflation-2.0.4.tgz", - "integrity": "sha512-daFI2atltBXImBIKT1FaETUQlEX3wAluTN0O4F0ukPA4CaK1DrYdGmqRU1CfWcyu/B7985DZ/28/Jk00R9pPOg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "@types/keygrip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", - "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", - "dev": true - }, - "@types/koa": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", - "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", - "dev": true, - "requires": { - "@types/accepts": "*", - "@types/content-disposition": "*", - "@types/cookies": "*", - "@types/http-assert": "*", - "@types/http-errors": "*", - "@types/keygrip": "*", - "@types/koa-compose": "*", - "@types/node": "*" - } - }, - "@types/koa-bodyparser": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.12.tgz", - "integrity": "sha512-hKMmRMVP889gPIdLZmmtou/BijaU1tHPyMNmcK7FAHAdATnRcGQQy78EqTTxLH1D4FTsrxIzklAQCso9oGoebQ==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, - "@types/koa-compose": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", - "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, - "@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "@types/mime-db": { - "version": "1.43.5", - "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.5.tgz", - "integrity": "sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==", - "dev": true - }, - "@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "@types/node": { - "version": "20.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", - "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==", - "dev": true, - "requires": { - "undici-types": "~5.26.4" - } - }, - "@types/nodemailer": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", - "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/on-finished": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.4.tgz", - "integrity": "sha512-Ld4UQD3udYcKPaAWlI1EYXKhefkZcTlpqOLkQRmN3u5Ml/tUypMivUHbNH8LweP4H4FlhGGO+uBjJI1Y1dkE1g==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/pako": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", - "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", - "dev": true - }, - "@types/psl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", - "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", - "dev": true - }, - "@types/qs": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", - "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/type-is": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", - "integrity": "sha512-fs1KHv/f9OvmTMsu4sBNaUu32oyda9Y9uK25naJG8gayxNrfqGIjPQsbLIYyfe7xFkppnPlJB+BuTldOaX9bXw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-i1aY7RKb6HmQIEnK0cBmUZUp1URx0riIHw/GYNoZ46Su0GWfLiDmMI8zMRmaauMnOTg2bQag0qfwcyUFC9Tn+A==", - "dev": true - }, - "@wdio/logger": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.28.0.tgz", - "integrity": "sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==", - "dev": true, - "peer": true, - "requires": { - "chalk": "^5.1.2", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "strip-ansi": "^7.1.0" - } - }, - "@wdio/reporter": { - "version": "8.32.4", - "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.32.4.tgz", - "integrity": "sha512-kZXbyNuZSSpk4kBavDb+ac25ODu9NVZED6WwZafrlMSnBHcDkoMt26Q0Jp3RKUj+FTyuKH0HvfeLrwVkk6QKDw==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "^20.1.0", - "@wdio/logger": "8.28.0", - "@wdio/types": "8.32.4", - "diff": "^5.0.0", - "object-inspect": "^1.12.0" - } - }, - "@wdio/types": { - "version": "8.32.4", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.32.4.tgz", - "integrity": "sha512-pDPGcCvq0MQF8u0sjw9m4aMI2gAKn6vphyBB2+1IxYriL777gbbxd7WQ+PygMBvYVprCYIkLPvhUFwF85WakmA==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "^20.1.0" - } - }, - "abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "dev": true - }, - "accept-language": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", - "integrity": "sha512-sUofgqBPzgfcF20sPoBYGQ1IhQLt2LSkxTnlQSuLF3n5gPEqd5AimbvOvHEi0T1kLMiGVqPWzI5a9OteBRth3A==", - "dev": true, - "requires": { - "bcp47": "^1.1.2", - "stable": "^0.1.6" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "peer": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "dev": true - }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "requires": { - "default-require-extensions": "^3.0.0" - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true - }, - "available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "requires": { - "possible-typed-array-names": "^1.0.0" - } - }, - "avvio": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.5.tgz", - "integrity": "sha512-AOhBxyLVdpOad3TujtC9kL/9r3HnTkxwQ5ggOsYrvvZP1cCFvzHWJd5XxZDFuTn+IN8vkKSG5SEJrd27vCSbeA==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1", - "queue-microtask": "^1.1.2" - } - }, - "aws-sdk": { - "version": "2.1592.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1592.0.tgz", - "integrity": "sha512-iwmS46jOEHMNodfrpNBJ5eHwjKAY05t/xYV2cp+KyzMX2yGgt2/EtWWnlcoMGBKR31qKTsjMj5ZPouC9/VeDOA==", - "dev": true, - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "dev": true - } - } - }, - "aws-sdk-mock": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.9.0.tgz", - "integrity": "sha512-kTUXaQQ1CTn3Cwxa2g1XqtCDq+FTEbPl/zgaYCok357f7gbWkeYEegqa5RziTRb11oNIUHrLp9DSHwZT3XdBkA==", - "dev": true, - "requires": { - "aws-sdk": "^2.1231.0", - "sinon": "^17.0.0", - "traverse": "^0.6.6" - }, - "dependencies": { - "@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": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0" - } - }, - "@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.5", - "supports-color": "^7.2.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcp47": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", - "integrity": "sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==", - "dev": true - }, - "binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true - }, - "bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", - "dev": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - }, - "dependencies": { - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - } - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "requires": { - "streamsearch": "^1.1.0" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true - }, - "cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "requires": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - } - }, - "caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "requires": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - } - }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - } - }, - "camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "requires": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001605", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz", - "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==", - "dev": true - }, - "capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, - "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "peer": true - }, - "change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "requires": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true - }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cldrjs": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", - "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "dev": true - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dev": true, - "requires": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true - } - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, - "cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", - "dev": true, - "requires": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - } - }, - "core-js": { - "version": "3.36.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", - "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "requires": { - "node-fetch": "^2.6.12" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true - }, - "crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, - "dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", - "dev": true - }, - "deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true - }, - "default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", - "dev": true, - "requires": { - "strip-bom": "^4.0.0" - } - }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true - }, - "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true - }, - "dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true - }, - "dotenv-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dotenv-json/-/dotenv-json-1.0.0.tgz", - "integrity": "sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==", - "dev": true - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", - "dev": true, - "requires": { - "jake": "^10.8.5" - } - }, - "electron-to-chromium": { - "version": "1.4.726", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.726.tgz", - "integrity": "sha512-xtjfBXn53RORwkbyKvDfTajtnTp0OJoPOIBzXvkNbb7+YYvCHJflba3L7Txyx/6Fov3ov2bGPr/n5MTixmPhdQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "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 - }, - "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "dev": true - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dev": true, - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "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-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-json-stringify": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", - "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", - "dev": true, - "requires": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "rfdc": "^1.2.0", - "string-similarity": "^4.0.1" - } - }, - "fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "fastify": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.18.1.tgz", - "integrity": "sha512-OA0imy/bQCMzf7LUCb/1JI3ZSoA0Jo0MLpYULxV7gpppOpJ8NBxDp2PQoQ0FDqJevZPb7tlZf5JacIQft8x9yw==", - "dev": true, - "requires": { - "@fastify/ajv-compiler": "^1.0.0", - "abstract-logging": "^2.0.0", - "avvio": "^7.1.2", - "fast-json-stringify": "^2.5.2", - "fastify-error": "^0.3.0", - "fastify-warning": "^0.2.0", - "find-my-way": "^4.0.0", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.2.1", - "proxy-addr": "^2.0.7", - "readable-stream": "^3.4.0", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^7.0.0" - } - }, - "fastify-error": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", - "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==", - "dev": true - }, - "fastify-warning": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", - "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==", - "dev": true - }, - "fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "requires": { - "minimatch": "^5.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-my-way": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", - "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", - "dev": true, - "requires": { - "fast-decode-uri-component": "^1.0.1", - "fast-deep-equal": "^3.1.3", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", - "dev": true - }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "dev": true - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true - }, - "fromentries": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "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 - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "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" - } - }, - "globalize": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", - "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", - "dev": true, - "requires": { - "cldrjs": "^0.5.4" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "requires": { - "es-define-property": "^1.0.0" - } - }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.3" - } - }, - "hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "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 - }, - "header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "requires": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, - "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 - }, - "http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" - } - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true - } - } - }, - "http-status": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.7.4.tgz", - "integrity": "sha512-c2qSwNtTlHVYAhMj9JpGdyo0No/+DiKXCJ9pHtZ2Yf3QmPnBIytKSRT7BuyIiQ7icXLynavGmxUqkOjSrAuMuA==", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "hyperid": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.3.1.tgz", - "integrity": "sha512-mIbI7Ymn6MCdODaW1/6wdf5lvvXzmPsARN4zTLakMmcziBOuP4PxCBJvHF6kbAIHX6H4vAELx/pDmt0j6Th5RQ==", - "dev": true, - "requires": { - "uuid": "^8.3.2", - "uuid-parse": "^1.1.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "invert-kv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", - "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "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-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "requires": { - "which-typed-array": "^1.1.14" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "requires": { - "append-transform": "^2.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - } - }, - "istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "requires": { - "semver": "^7.5.3" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", - "dev": true, - "requires": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "dev": true - }, - "joi": { - "version": "17.12.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", - "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "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" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-merge-patch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-1.0.2.tgz", - "integrity": "sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "json-schema-compare": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", - "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", - "dev": true, - "requires": { - "lodash": "^4.17.4" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, - "jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - } - }, - "jssha": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", - "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", - "dev": true - }, - "just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "requires": { - "tsscmp": "1.0.6" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "koa": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.2.tgz", - "integrity": "sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA==", - "dev": true, - "requires": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.9.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "dependencies": { - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true - } - } - }, - "koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "requires": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - } - }, - "lambda-event-mock": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lambda-event-mock/-/lambda-event-mock-1.5.0.tgz", - "integrity": "sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==", - "dev": true, - "requires": { - "@extra-number/significant-digits": "^1.1.1", - "clone-deep": "^4.0.1", - "uuid": "^3.3.3", - "vandium-utils": "^1.2.0" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "vandium-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-1.2.0.tgz", - "integrity": "sha512-yxYUDZz4BNo0CW/z5w4mvclitt5zolY7zjW97i6tTE+sU63cxYs1A6Bl9+jtIQa3+0hkeqY87k+7ptRvmeHe3g==", - "dev": true - } - } - }, - "lambda-leak": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lambda-leak/-/lambda-leak-2.0.0.tgz", - "integrity": "sha512-2c9jwUN3ZLa2GEiOhObbx2BMGQplEUCDHSIkhDtYwUjsTfiV/3jCF6ThIuEXfsvqbUK+0QpZcugIKB8YMbSevQ==", - "dev": true - }, - "lambda-tester": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lambda-tester/-/lambda-tester-4.0.1.tgz", - "integrity": "sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==", - "dev": true, - "requires": { - "app-root-path": "^3.0.0", - "dotenv": "^8.0.0", - "dotenv-json": "^1.0.0", - "lambda-event-mock": "^1.5.0", - "lambda-leak": "^2.0.0", - "semver": "^6.1.1", - "uuid": "^3.3.3", - "vandium-utils": "^2.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } - } - }, - "lcid": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", - "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", - "dev": true, - "requires": { - "invert-kv": "^3.0.0" - } - }, - "libphonenumber-js": { - "version": "1.10.59", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.59.tgz", - "integrity": "sha512-HeTsOrDF/hWhEiKqZVwg9Cqlep5x2T+IYDENvT2VRj3iX8JQ7Y+omENv+AIn0vC8m6GYhivfCed5Cgfw27r5SA==" - }, - "light-my-request": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.12.0.tgz", - "integrity": "sha512-0y+9VIfJEsPVzK5ArSIJ8Dkxp8QMP7/aCuxCUtG/tr9a2NoOf/snATE/OUc05XUplJCEnRh6gTkH7xh9POt1DQ==", - "dev": true, - "requires": { - "ajv": "^8.1.0", - "cookie": "^0.5.0", - "process-warning": "^1.0.0", - "set-cookie-parser": "^2.4.1" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "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.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "loglevel": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", - "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", - "dev": true, - "peer": true - }, - "loglevel-plugin-prefix": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", - "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", - "dev": true, - "peer": true - }, - "loopback-connector": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-5.3.3.tgz", - "integrity": "sha512-ZYULfy5W7+R2A3I9TILWZOdfMVcZ2qEQT/tye0Fy7Ju3zQ6Gv1bmroARGPGVDAneFt+5YaiaieLdoJ1t02hLpg==", - "dev": true, - "requires": { - "async": "^3.2.4", - "bluebird": "^3.7.2", - "debug": "^4.3.4", - "msgpack5": "^4.5.1", - "strong-globalize": "^6.0.5", - "uuid": "^9.0.0" - }, - "dependencies": { - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true - } - } - }, - "loopback-datasource-juggler": { - "version": "4.28.9", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.28.9.tgz", - "integrity": "sha512-vBwqQaSa2GpCqS/zevAGG6zRgzsQ/KhB4xUaBSbGxNMD6GwTbS60GuD4yKSN2t4pwx4Qca2x3YUAXhumO1bN2Q==", - "dev": true, - "requires": { - "async": "^3.2.4", - "change-case": "^4.1.2", - "debug": "^4.3.4", - "depd": "^2.0.0", - "inflection": "^1.13.4", - "lodash": "^4.17.21", - "loopback-connector": "^5.3.3", - "minimatch": "^5.1.6", - "nanoid": "^3.3.6", - "qs": "^6.11.2", - "strong-globalize": "^6.0.5", - "traverse": "^0.6.7", - "uuid": "^9.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true - } - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.1" - } - }, - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "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, - "requires": { - "yallist": "^3.0.2" - } - }, - "lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true - }, - "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, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "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 - }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true - }, - "mem": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", - "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^2.1.0", - "p-is-promise": "^2.1.0" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "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 - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "mocha": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "mocha-split-tests": { - "version": "git+ssh://git@github.com/rishabhpoddar/mocha-split-tests.git#b0bd99a7d5870493dbe921dbdd5195b47e555035", - "dev": true, - "from": "mocha-split-tests@github:rishabhpoddar/mocha-split-tests", - "requires": { - "commander": "^7.0.0", - "glob": "^7.1.6" - } - }, - "mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "msgpack5": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.5.1.tgz", - "integrity": "sha512-zC1vkcliryc4JGlL6OfpHumSYUHWFGimSI+OgfRCjTFLmKA2/foR9rMTOhWiqfOrfxJOctrpWPvrppf8XynJxw==", - "dev": true, - "requires": { - "bl": "^2.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.3.6", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", - "dev": true, - "requires": { - "@next/env": "14.1.4", - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4", - "@swc/helpers": "0.5.2", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", - "postcss": "8.4.31", - "styled-jsx": "5.1.1" - } - }, - "next-test-api-route-handler": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/next-test-api-route-handler/-/next-test-api-route-handler-3.2.0.tgz", - "integrity": "sha512-gEev0YpErOjcfGY6Vj50xKAFBYCymYTdVCQuid1rqY2NIbA99GrTIEs79j6FF7+6j2R6ruQPcbwt+z0f9Z1J9w==", - "dev": true, - "requires": { - "cookie": "^0.6.0", - "core-js": "^3.35.0", - "node-fetch": "^2.6.7" - }, - "dependencies": { - "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true - } - } - }, - "nise": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/text-encoding": "^0.7.2", - "just-extend": "^6.2.0", - "path-to-regexp": "^6.2.1" - }, - "dependencies": { - "@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": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0" - } - } - } - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "nock": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz", - "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==", - "dev": true, - "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.13", - "mkdirp": "^0.5.0", - "propagate": "^2.0.0" - } - }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "requires": { - "process-on-spawn": "^1.0.0" - } - }, - "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "nodemailer": { - "version": "6.9.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", - "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "nyc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", - "dev": true, - "requires": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, - "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, - "openapi3-ts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", - "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", - "dev": true, - "requires": { - "yaml": "^1.10.2" - } - }, - "os-locale": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", - "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", - "dev": true, - "requires": { - "execa": "^4.0.0", - "lcid": "^3.0.0", - "mem": "^5.0.0" - } - }, - "otpauth": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.1.5.tgz", - "integrity": "sha512-mnic91MZxvj04Ir7FN8Xi6wF3FU8D+s6M5p6FQaSS91/csKswoOI9Dk7kKSnGFAoBYgGTTO+OWScV0nJuzrbPg==", - "dev": true, - "requires": { - "jssha": "~3.3.1" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true - }, - "p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "requires": { - "p-timeout": "^3.1.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, - "param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dev": true, - "requires": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - } - }, - "pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", - "dev": true - }, - "pkce-challenge": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.1.0.tgz", - "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==", - "requires": { - "crypto-js": "^4.1.1" - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - } - } - }, - "possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true - }, - "pretty-quick": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.3.1.tgz", - "integrity": "sha512-3b36UXfYQ+IXXqex6mCca89jC8u0mYLqFAN5eTQKoXO6oCQYcIVYZEB/5AlBHI7JPYygReM2Vv6Vom/Gln7fBg==", - "dev": true, - "requires": { - "execa": "^4.1.0", - "find-up": "^4.1.0", - "ignore": "^5.3.0", - "mri": "^1.2.0", - "picocolors": "^1.0.0", - "picomatch": "^3.0.1", - "tslib": "^2.6.2" - }, - "dependencies": { - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-try": "^2.0.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - }, - "picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true - } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "dev": true, - "requires": { - "fromentries": "^1.2.0" - } - }, - "process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "dev": true - }, - "propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, - "qs": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", - "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", - "requires": { - "side-channel": "^1.0.6" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "dev": true - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - } - } - }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "dev": true - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "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 - }, - "ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", - "dev": true, - "requires": { - "ret": "~0.2.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", - "dev": true - }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, - "secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true - }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", - "dev": true - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true - }, - "set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shiki": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", - "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", - "dev": true, - "requires": { - "jsonc-parser": "^3.0.0", - "vscode-oniguruma": "^1.6.1", - "vscode-textmate": "5.2.0" - } - }, - "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "requires": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "sinon": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", - "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^7.0.1", - "diff": "^5.0.0", - "nise": "^5.1.2", - "supports-color": "^7.2.0" - }, - "dependencies": { - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dev": true, - "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true - }, - "spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "requires": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - }, - "stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true - }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-similarity": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", - "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "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, - "peer": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "strong-error-handler": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-4.0.8.tgz", - "integrity": "sha512-8C4DoE7/0YTKcrhVcT1Wz/aIXXxBWi4H0WOKTKabuv2q1wSgdkPOgBUSsyp8U34e+aEOtX0CqIkUK/JaNG94QA==", - "dev": true, - "requires": { - "accepts": "^1.3.8", - "debug": "^4.3.4", - "ejs": "^3.1.9", - "fast-safe-stringify": "^2.1.1", - "http-status": "^1.6.2", - "js2xmlparser": "^4.0.2", - "strong-globalize": "^6.0.5" - } - }, - "strong-globalize": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-6.0.6.tgz", - "integrity": "sha512-+mN0wTXBg9rLiKBk7jsyfXFWsg08q160XQcmJ3gNxSQ8wrC668dzR8JUp/wcK3NZ2eQ5h5tvc8O6Y+FC0D61lw==", - "dev": true, - "requires": { - "accept-language": "^3.0.18", - "debug": "^4.2.0", - "globalize": "^1.6.0", - "lodash": "^4.17.20", - "md5": "^2.3.0", - "mkdirp": "^1.0.4", - "os-locale": "^5.0.0", - "yamljs": "^0.3.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } - } - }, - "styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "dev": true, - "requires": { - "client-only": "0.0.1" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, - "supertokens-js-override": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz", - "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "tiny-lru": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", - "dev": true - }, - "tldts": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.48.tgz", - "integrity": "sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==", - "requires": { - "tldts-core": "^6.1.48" - } - }, - "tldts-core": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.48.tgz", - "integrity": "sha512-3gD9iKn/n2UuFH1uilBviK9gvTNT6iYwdqrj1Vr5mh8FuelvpRNaYVH4pNYqUgOGU4aAdL9X35eLuuj0gRsx+A==" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "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" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true - }, - "toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", - "dev": true - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "traverse": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", - "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", - "dev": true - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true - }, - "twilio": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.23.0.tgz", - "integrity": "sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==", - "requires": { - "axios": "^1.6.0", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.0", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "url-parse": "^1.5.9", - "xmlbuilder": "^13.0.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typedoc": { - "version": "0.22.18", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.18.tgz", - "integrity": "sha512-NK9RlLhRUGMvc6Rw5USEYgT4DVAUFk7IF7Q6MYfpJ88KnTZP7EneEa4RcP+tX1auAcz7QT1Iy0bUSZBYYHdoyA==", - "dev": true, - "requires": { - "glob": "^8.0.3", - "lunr": "^2.3.9", - "marked": "^4.0.16", - "minimatch": "^5.1.0", - "shiki": "^0.10.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true - }, - "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true - } - } - }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true - }, - "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 - }, - "uuid-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", - "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", - "dev": true - }, - "validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "dev": true - }, - "vandium-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-2.0.0.tgz", - "integrity": "sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true - }, - "vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true - }, - "vscode-textmate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", - "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true - }, - "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - } - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true - } - } - }, - "xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" - }, - "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", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - } - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - } - } - }, - "ylru": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", - "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/package.json b/package.json index e12521494..c1bddbf95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "20.1.3", + "version": "21.0.0", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { @@ -124,6 +124,7 @@ "pako": "^2.1.0", "pkce-challenge": "^3.0.0", "process": "^0.11.10", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "tldts": "^6.1.48", "twilio": "^4.19.3" @@ -146,7 +147,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/pako": "^2.0.3", - "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", diff --git a/recipe/oauth2client/index.d.ts b/recipe/oauth2client/index.d.ts new file mode 100644 index 000000000..89f4241f8 --- /dev/null +++ b/recipe/oauth2client/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/oauth2client"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/oauth2client"; +export default _default; diff --git a/recipe/oauth2client/index.js b/recipe/oauth2client/index.js new file mode 100644 index 000000000..f1b31d6db --- /dev/null +++ b/recipe/oauth2client/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/oauth2client")); diff --git a/recipe/oauth2client/types/index.d.ts b/recipe/oauth2client/types/index.d.ts new file mode 100644 index 000000000..e475d4576 --- /dev/null +++ b/recipe/oauth2client/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/oauth2client/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/oauth2client/types"; +export default _default; diff --git a/recipe/oauth2client/types/index.js b/recipe/oauth2client/types/index.js new file mode 100644 index 000000000..01b5c40c6 --- /dev/null +++ b/recipe/oauth2client/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/oauth2client/types")); diff --git a/recipe/oauth2provider/index.d.ts b/recipe/oauth2provider/index.d.ts new file mode 100644 index 000000000..72d8d7e7d --- /dev/null +++ b/recipe/oauth2provider/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/oauth2provider"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/oauth2provider"; +export default _default; diff --git a/recipe/oauth2provider/index.js b/recipe/oauth2provider/index.js new file mode 100644 index 000000000..1b9fa1030 --- /dev/null +++ b/recipe/oauth2provider/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/oauth2provider")); diff --git a/recipe/oauth2provider/types/index.d.ts b/recipe/oauth2provider/types/index.d.ts new file mode 100644 index 000000000..bdfe9d82c --- /dev/null +++ b/recipe/oauth2provider/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/oauth2provider/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/oauth2provider/types"; +export default _default; diff --git a/recipe/oauth2provider/types/index.js b/recipe/oauth2provider/types/index.js new file mode 100644 index 000000000..d508e2566 --- /dev/null +++ b/recipe/oauth2provider/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/oauth2provider/types")); diff --git a/recipe/openid/index.d.ts b/recipe/openid/index.d.ts new file mode 100644 index 000000000..64f95c7b5 --- /dev/null +++ b/recipe/openid/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/openid"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/openid"; +export default _default; diff --git a/recipe/openid/index.js b/recipe/openid/index.js new file mode 100644 index 000000000..276ef8f9d --- /dev/null +++ b/recipe/openid/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/openid")); diff --git a/recipe/openid/types/index.d.ts b/recipe/openid/types/index.d.ts new file mode 100644 index 000000000..2c15f75f4 --- /dev/null +++ b/recipe/openid/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/openid/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/openid/types"; +export default _default; diff --git a/recipe/openid/types/index.js b/recipe/openid/types/index.js new file mode 100644 index 000000000..26a5b76d2 --- /dev/null +++ b/recipe/openid/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/openid/types")); diff --git a/test/auth-react-server/index.js b/test/auth-react-server/index.js index 3b552a1a9..51ad8fa28 100644 --- a/test/auth-react-server/index.js +++ b/test/auth-react-server/index.js @@ -51,6 +51,9 @@ const TOTPRaw = require("../../lib/build/recipe/totp/recipe").default; const TOTP = require("../../recipe/totp"); const OTPAuth = require("otpauth"); +const OAuth2ProviderRaw = require("../../lib/build/recipe/oauth2provider/recipe").default; +const OAuth2Provider = require("../../recipe/oauth2provider"); + let { startST, killAllST, @@ -502,6 +505,15 @@ app.post("/test/getTOTPCode", (req, res) => { res.send(JSON.stringify({ totp: new OTPAuth.TOTP({ secret: req.body.secret, digits: 6, period: 1 }).generate() })); }); +app.post("/test/create-oauth2-client", async (req, res, next) => { + try { + const { client } = await OAuth2Provider.createOAuth2Client(req.body); + res.send({ client }); + } catch (e) { + next(e); + } +}); + app.get("/test/featureFlags", (req, res) => { const available = []; @@ -515,6 +527,7 @@ app.get("/test/featureFlags", (req, res) => { available.push("mfa"); available.push("recipeConfig"); available.push("accountlinking-fixes"); // this is related to 19.0 release in which we fixed a bunch of issues with account linking, including changing error codes. + available.push("oauth2"); res.send({ available, @@ -568,6 +581,7 @@ function initST({ passwordlessConfig } = {}) { UserMetadataRaw.reset(); MultiFactorAuthRaw.reset(); TOTPRaw.reset(); + OAuth2ProviderRaw.reset(); SuperTokensRaw.reset(); passwordlessConfig = { @@ -937,6 +951,8 @@ function initST({ passwordlessConfig } = {}) { }), ]); + recipeList.push(["oauth2provider", OAuth2Provider.init()]); + SuperTokens.init({ appInfo: { appName: "SuperTokens", diff --git a/test/config.test.js b/test/config.test.js index d81653329..7824a94ae 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -234,7 +234,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], }); SessionRecipe.getInstanceOrThrowError(); - assert.strictEqual(SuperTokens.getInstanceOrThrowError().recipeModules.length, 3); // multitenancy&usermetadata is initialised by default + assert.strictEqual(SuperTokens.getInstanceOrThrowError().recipeModules.length, 4); // multitenancy&usermetadata&oauth2provider is initialised by default resetAll(); } @@ -252,7 +252,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); SessionRecipe.getInstanceOrThrowError(); EmailPasswordRecipe.getInstanceOrThrowError(); - assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 4); // multitenancy&usermetadata is initialised by default + assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 5); // multitenancy&usermetadata&oauth2provider is initialised by default resetAll(); } }); diff --git a/test/emailpassword/passwordreset.test.js b/test/emailpassword/passwordreset.test.js index f4d43b106..e8a579f6f 100644 --- a/test/emailpassword/passwordreset.test.js +++ b/test/emailpassword/passwordreset.test.js @@ -111,6 +111,48 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] assert(response.body.formFields[0].id === "email"); }); + it("test invalid email type in generate token API", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: 123456, + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response.body.message === "email value must be a string"); + }); + it("test that generated password link is correct", async function () { const connectionURI = await startST(); @@ -255,6 +297,49 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] assert(response.status !== "FIELD_ERROR"); }); + it("test invalid type of password", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: 12345, + }, + ], + token: "randomToken", + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(response.message === "password value must be a string"); + }); + it("test token missing from input", async function () { const connectionURI = await startST(); STExpress.init({ diff --git a/test/emailpassword/signinFeature.test.js b/test/emailpassword/signinFeature.test.js index 21e6e0d5c..15f95f7d6 100644 --- a/test/emailpassword/signinFeature.test.js +++ b/test/emailpassword/signinFeature.test.js @@ -351,7 +351,7 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] } }) ); - assert(JSON.parse(res.text).message === "The value of formFields with id = password must be a string"); + assert(JSON.parse(res.text).message === "password value must be a string"); }); it("test email must be of type string in input", async function () { @@ -404,7 +404,7 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] } }) ); - assert(JSON.parse(res.text).message === "The value of formFields with id = email must be a string"); + assert(JSON.parse(res.text).message === "email value must be a string"); }); /* diff --git a/test/emailpassword/signupFeature.test.js b/test/emailpassword/signupFeature.test.js index 4f337ef8b..c9a5299ab 100644 --- a/test/emailpassword/signupFeature.test.js +++ b/test/emailpassword/signupFeature.test.js @@ -357,6 +357,102 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert.strictEqual(badInputResponse.message, "Missing input param: formFields"); }); + it("test bad input, invalid password type in /signup API", async function () { + const connectionURI = await startST(); + + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let badInputResponse = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "random@gmail.com", + }, + { + id: "password", + value: 1234, + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(badInputResponse.message === "password value must be a string"); + }); + + it("test bad input, invalid email type in /signup API", async function () { + const connectionURI = await startST(); + + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let badInputResponse = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: 12435, + }, + { + id: "password", + value: "1234", + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(badInputResponse.message === "email value must be a string"); + }); + it("test bad input, formFields is not an array in /signup API", async function () { const connectionURI = await startST(); diff --git a/test/jwt/getJWKS.test.js b/test/jwt/getJWKS.test.js index 5c4eab262..e9593cc12 100644 --- a/test/jwt/getJWKS.test.js +++ b/test/jwt/getJWKS.test.js @@ -118,9 +118,12 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { assert(Object.keys(response).length === 1); assert(response.keys !== undefined); assert(response.keys.length > 0); - assert(headers["cache-control"].includes(", must-revalidate")); - assert(headers["cache-control"].includes("max-age=")); - assert(!headers["cache-control"].includes("max-age=60,")); + const cacheControlHeaderParts = headers["cache-control"].split(", "); + assert.strictEqual(cacheControlHeaderParts.length, 2); + assert(cacheControlHeaderParts[0].startsWith("max-age=60")); + const maxAge = Number.parseInt(cacheControlHeaderParts[0].split("=")[1]); + assert(maxAge >= 60); + assert.strictEqual(cacheControlHeaderParts[1], "must-revalidate"); }); it("Test that we can override the Cache-Control header through the function", async function () { diff --git a/test/oauth2/config.test.js b/test/oauth2/config.test.js new file mode 100644 index 000000000..5d2858925 --- /dev/null +++ b/test/oauth2/config.test.js @@ -0,0 +1,45 @@ +let assert = require("assert"); + +const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); +let { ProcessState } = require("../../lib/build/processState"); +let STExpress = require("../../"); +const OAuth2ProviderRecipe = require("../../lib/build/recipe/oauth2provider/recipe").default; +let { Querier } = require("../../lib/build/querier"); +const { maxVersion } = require("../../lib/build/utils"); + +describe(`configTest: ${printPath("[test/oauth2/config.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("Test that the recipe initializes without a config obj", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2ProviderRecipe.init()], + }); + + // Only run for version >= 2.9 + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.8") === "2.8") { + return; + } + + OAuth2ProviderRecipe.getInstanceOrThrowError(); + }); +}); diff --git a/test/oauth2/oauth2client.test.js b/test/oauth2/oauth2client.test.js new file mode 100644 index 000000000..a95af4fe8 --- /dev/null +++ b/test/oauth2/oauth2client.test.js @@ -0,0 +1,205 @@ +let assert = require("assert"); + +const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); +let { ProcessState } = require("../../lib/build/processState"); +let STExpress = require("../../"); +let OAuth2Recipe = require("../../recipe/oauth2provider"); + +describe(`OAuth2ClientTests: ${printPath("[test/oauth2/oauth2client.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("should create an OAuth2Client instance with empty input", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + const { client } = await OAuth2Recipe.createOAuth2Client({}); + + assert(client.clientId !== undefined); + assert(client.clientSecret !== undefined); + assert.strictEqual(client.scope, "offline_access offline openid"); + }); + + it("should create an OAuth2Client instance with custom input", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + const { client } = await OAuth2Recipe.createOAuth2Client( + { + clientName: "client_name", + }, + {} + ); + + assert.strictEqual(client.clientName, "client_name"); + }); + + it("should update the OAuth2Client", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create a client + const { client } = await OAuth2Recipe.createOAuth2Client( + { + scope: "offline_access offline", + redirectUris: ["http://localhost:3000"], + }, + {} + ); + + assert.strictEqual(client.scope, "offline_access offline"); + assert.strictEqual(JSON.stringify(client.redirectUris), JSON.stringify(["http://localhost:3000"])); + assert.strictEqual(JSON.stringify(client.metadata), JSON.stringify({})); + + // Update the client + const { client: updatedClient } = await OAuth2Recipe.updateOAuth2Client( + { + clientId: client.clientId, + clientSecret: "new_client_secret", + scope: "offline_access", + redirectUris: null, + metadata: { a: 1, b: 2 }, + }, + {} + ); + + assert.strictEqual(updatedClient.clientSecret, "new_client_secret"); + assert.strictEqual(updatedClient.scope, "offline_access"); + assert.strictEqual(updatedClient.redirectUris, null); + assert.strictEqual(JSON.stringify(updatedClient.metadata), JSON.stringify({ a: 1, b: 2 })); + }); + + it("should delete the OAuth2Client", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create a client + const { client } = await OAuth2Recipe.createOAuth2Client({}); + + assert.strictEqual(client.scope, "offline_access offline openid"); + + // Delete the client + const { status } = await OAuth2Recipe.deleteOAuth2Client( + { + clientId: client.clientId, + }, + {} + ); + + assert.strictEqual(status, "OK"); + }); + + it("should get OAuth2Clients with pagination", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + let clientIds = new Set(); + // Create 10 clients + for (let i = 0; i < 10; i++) { + const { client } = await OAuth2Recipe.createOAuth2Client({}); + clientIds.add(client.clientId); + } + + let allClients = []; + let nextPaginationToken = undefined; + + // Fetch clients in pages of 3 + do { + const result = await OAuth2Recipe.getOAuth2Clients( + { pageSize: 3, paginationToken: nextPaginationToken }, + {} + ); + assert.strictEqual(result.status, "OK"); + nextPaginationToken = result.nextPaginationToken; + allClients.push(...result.clients); + } while (nextPaginationToken); + + assert.strictEqual(allClients.length, 10); + // Check the client IDs + for (let i = 0; i < 10; i++) { + assert(clientIds.has(allClients[i].clientId)); + } + }); + + it("should get OAuth2Clients with filter", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create 5 clients with clientName = "customClientName" + for (let i = 0; i < 5; i++) { + await OAuth2Recipe.createOAuth2Client({ clientName: "customClientName" }); + } + + let result = await OAuth2Recipe.getOAuth2Clients({ clientName: "customClientName" }); + assert.strictEqual(result.status, "OK"); + assert.strictEqual(result.clients.length, 5); + }); +}); diff --git a/test/openid/config.test.js b/test/openid/config.test.js index 2a73550a0..1f7a52438 100644 --- a/test/openid/config.test.js +++ b/test/openid/config.test.js @@ -40,10 +40,7 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () return; } - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://api.supertokens.io"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === "/auth"); + assert((await OpenIdRecipe.getIssuer()) === "https://api.supertokens.io/auth"); }); it("Test that the default config sets values correctly for OpenID recipe with apiBasePath", async function () { @@ -68,10 +65,7 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () return; } - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://api.supertokens.io"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === ""); + assert((await OpenIdRecipe.getIssuer()) === "https://api.supertokens.io"); }); it("Test that the config sets values correctly for OpenID recipe with issuer", async function () { @@ -88,7 +82,18 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }, recipeList: [ OpenIdRecipe.init({ - issuer: "https://customissuer.com", + override: { + functions: (originalImplementation) => ({ + ...originalImplementation, + getOpenIdDiscoveryConfiguration: async (input) => { + const orig = originalImplementation.getOpenIdDiscoveryConfiguration(input); + return { + ...orig, + issuer: "https://customissuer.com", + }; + }, + }), + }, }), ], }); @@ -100,38 +105,7 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () return; } - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://customissuer.com"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === ""); - }); - - it("Test that issuer without apiBasePath throws error", async function () { - const connectionURI = await startST(); - - try { - STExpress.init({ - supertokens: { - connectionURI, - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://customissuer.com", - }), - ], - }); - } catch (e) { - if ( - e.message !== "The path of the issuer URL must be equal to the apiBasePath. The default value is /auth" - ) { - throw e; - } - } + assert((await OpenIdRecipe.getIssuer()) === "https://customissuer.com"); }); it("Test that issuer with gateway path works fine", async function () { @@ -156,9 +130,6 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () return; } - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert.equal(openIdRecipe.config.issuerDomain.getAsStringDangerous(), "https://api.supertokens.io"); - assert.equal(openIdRecipe.config.issuerPath.getAsStringDangerous(), "/gateway/auth"); + assert.equal(await OpenIdRecipe.getIssuer(), "https://api.supertokens.io/gateway/auth"); }); }); diff --git a/test/openid/openid.test.js b/test/openid/openid.test.js index 657f4958d..909a412f6 100644 --- a/test/openid/openid.test.js +++ b/test/openid/openid.test.js @@ -88,7 +88,19 @@ describe(`openIdTest: ${printPath("[test/openid/openid.test.js]")}`, function () }, recipeList: [ OpenIdRecipe.init({ - issuer: "https://cusomissuer/auth", + override: { + functions: (originalImplementation) => ({ + ...originalImplementation, + getOpenIdDiscoveryConfiguration: async (input) => { + const orig = originalImplementation.getOpenIdDiscoveryConfiguration(input); + return { + ...orig, + issuer: "https://cusomissuer/auth", + jwks_uri: "https://cusomissuer/auth/jwt/jwks.json", + }; + }, + }), + }, }), ], }); diff --git a/test/querier.test.js b/test/querier.test.js index 22c9646da..64ff73480 100644 --- a/test/querier.test.js +++ b/test/querier.test.js @@ -193,7 +193,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { assert.equal(await q.sendPostRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); let hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 2); - assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); // this will be the 4th API call + assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}, {}, {}), "Hello\n"); // this will be the 4th API call hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 2); assert.equal(hostsAlive.has(connectionURI), true); diff --git a/test/session/jwksCache.test.js b/test/session/jwksCache.test.js index 3047e819d..e86c53e37 100644 --- a/test/session/jwksCache.test.js +++ b/test/session/jwksCache.test.js @@ -211,16 +211,18 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct SuperTokens.convertToRecipeUserId("test-user-id") ); const tokens = createRes.getAllSessionTokensDangerously(); + clock.tick(500); assert.strictEqual(requestMock.callCount, 0); assert.ok(await Session.getSessionWithoutRequestResponse(tokens.accessToken, tokens.antiCsrfToken)); assert.strictEqual(requestMock.callCount, 1); + clock.tick(1000); // This should be done using the cache assert.ok(await Session.getSessionWithoutRequestResponse(tokens.accessToken, tokens.antiCsrfToken)); assert.strictEqual(requestMock.callCount, 1); - // we "wait" for 3 seconds to make the cache out-of-date - clock.tick(20000); + // we "wait" for 21 seconds to make the cache out-of-date + clock.tick(21000); // This should re-fetch from the core assert.ok(await Session.getSessionWithoutRequestResponse(tokens.accessToken, tokens.antiCsrfToken)); assert.strictEqual(requestMock.callCount, 2); diff --git a/test/session/overwriteSessionDuringSignInUp.test.js b/test/session/overwriteSessionDuringSignInUp.test.js index 3bf12c781..dd397811a 100644 --- a/test/session/overwriteSessionDuringSignInUp.test.js +++ b/test/session/overwriteSessionDuringSignInUp.test.js @@ -119,7 +119,7 @@ describe(`overwriteSessionDuringSignInUp config: ${printPath( ); cookies = extractInfoFromResponse(res); - assert(cookies.accessTokenFromAny === undefined); + assert.notStrictEqual(cookies.accessTokenFromAny, undefined); }); it("test false", async function () { diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index 2062bae25..e41f2f41e 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -19,6 +19,11 @@ import ThirdPartyRecipe from "../../../lib/build/recipe/thirdparty/recipe"; import { TypeInput as ThirdPartyTypeInput } from "../../../lib/build/recipe/thirdparty/types"; import { TypeInput as MFATypeInput } from "../../../lib/build/recipe/multifactorauth/types"; import TOTPRecipe from "../../../lib/build/recipe/totp/recipe"; +import OAuth2ProviderRecipe from "../../../lib/build/recipe/oauth2provider/recipe"; +import { TypeInput as OAuth2ProviderTypeInput } from "../../../lib/build/recipe/oauth2provider/types"; +import OAuth2ClientRecipe from "../../../lib/build/recipe/oauth2client/recipe"; +import { TypeInput as OAuth2ClientTypeInput } from "../../../lib/build/recipe/oauth2client/types"; +import { TypeInput as OpenIdRecipeTypeInput } from "../../../lib/build/recipe/openid/types"; import UserMetadataRecipe from "../../../lib/build/recipe/usermetadata/recipe"; import SuperTokensRecipe from "../../../lib/build/supertokens"; import { RecipeListFunction } from "../../../lib/build/types"; @@ -32,6 +37,8 @@ import Session from "../../../recipe/session"; import { verifySession } from "../../../recipe/session/framework/express"; import ThirdParty from "../../../recipe/thirdparty"; import TOTP from "../../../recipe/totp"; +import OAuth2Provider from "../../../recipe/oauth2provider"; +import OAuth2Client from "../../../recipe/oauth2client"; import accountlinkingRoutes from "./accountlinking"; import emailpasswordRoutes from "./emailpassword"; import emailverificationRoutes from "./emailverification"; @@ -39,6 +46,7 @@ import { logger } from "./logger"; import multiFactorAuthRoutes from "./multifactorauth"; import multitenancyRoutes from "./multitenancy"; import passwordlessRoutes from "./passwordless"; +import OAuth2ProviderRoutes from "./oauth2provider"; import sessionRoutes from "./session"; import supertokensRoutes from "./supertokens"; import thirdPartyRoutes from "./thirdparty"; @@ -91,6 +99,8 @@ function STReset() { ProcessState.getInstance().reset(); MultiFactorAuthRecipe.reset(); TOTPRecipe.reset(); + OAuth2ProviderRecipe.reset(); + OAuth2ClientRecipe.reset(); SuperTokensRecipe.reset(); DashboardRecipe.reset(); } @@ -291,15 +301,48 @@ function initST(config: any) { TOTP.init({ ...config, override: { - apis: overrideBuilderWithLogging("Multitenancy.override.apis", config?.override?.apis), - functions: overrideBuilderWithLogging( - "Multitenancy.override.functions", - config?.override?.functions - ), + apis: overrideBuilderWithLogging("TOTP.override.apis", config?.override?.apis), + functions: overrideBuilderWithLogging("TOTP.override.functions", config?.override?.functions), }, }) ); } + if (recipe.recipeId === "oauth2provider") { + let initConfig: OAuth2ProviderTypeInput = { + ...config, + }; + if (initConfig.override?.functions) { + initConfig.override = { + ...initConfig.override, + functions: getFunc(`${initConfig.override.functions}`), + }; + } + if (initConfig.override?.apis) { + initConfig.override = { + ...initConfig.override, + apis: getFunc(`${initConfig.override.apis}`), + }; + } + recipeList.push(OAuth2Provider.init(initConfig)); + } + if (recipe.recipeId === "oauth2client") { + let initConfig: OAuth2ClientTypeInput = { + ...config, + }; + if (initConfig.override?.functions) { + initConfig.override = { + ...initConfig.override, + functions: getFunc(`${initConfig.override.functions}`), + }; + } + if (initConfig.override?.apis) { + initConfig.override = { + ...initConfig.override, + apis: getFunc(`${initConfig.override.apis}`), + }; + } + recipeList.push(OAuth2Client.init(initConfig)); + } }); init.recipeList = recipeList; @@ -391,6 +434,7 @@ app.use("/test/multifactorauth", multiFactorAuthRoutes); app.use("/test/thirdparty", thirdPartyRoutes); app.use("/test/totp", TOTPRoutes); app.use("/test/usermetadata", userMetadataRoutes); +app.use("/test/oauth2provider", OAuth2ProviderRoutes); // *** Custom routes to help with session tests *** app.post("/create", async (req, res, next) => { diff --git a/test/test-server/src/oauth2provider.ts b/test/test-server/src/oauth2provider.ts new file mode 100644 index 000000000..1da664e3c --- /dev/null +++ b/test/test-server/src/oauth2provider.ts @@ -0,0 +1,75 @@ +import { Router } from "express"; +import OAuth2Provider from "../../../recipe/oauth2provider"; +import { logger } from "./logger"; + +const namespace = "com.supertokens:node-test-server:oauth2provider"; +const { logDebugMessage } = logger(namespace); + +const router = Router() + .post("/getoauth2clients", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:getOAuth2Clients %j", req.body); + const response = await OAuth2Provider.getOAuth2Clients(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/createoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:createOAuth2Client %j", req.body); + const response = await OAuth2Provider.createOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/updateoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:updateOAuth2Client %j", req.body); + const response = await OAuth2Provider.updateOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/deleteoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:deleteOAuth2Client %j", req.body); + const response = await OAuth2Provider.deleteOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/validateoauth2accesstoken", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:validateOAuth2AccessToken %j", req.body); + const response = await OAuth2Provider.validateOAuth2AccessToken( + req.body.token, + req.body.requirements, + req.body.checkDatabase, + req.body.userContext + ); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/createtokenforclientcredentials", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:createTokenForClientCredentials %j", req.body); + const response = await OAuth2Provider.createTokenForClientCredentials( + req.body.clientId, + req.body.clientSecret, + req.body.scope, + req.body.audience, + req.body.userContext + ); + res.json(response); + } catch (e) { + next(e); + } + }); + +export default router; diff --git a/test/utils.js b/test/utils.js index ff359390d..f679a6c3a 100644 --- a/test/utils.js +++ b/test/utils.js @@ -31,6 +31,7 @@ let PasswordlessRecipe = require("..//lib/build/recipe/passwordless/recipe").def let MultitenancyRecipe = require("../lib/build/recipe/multitenancy/recipe").default; let MultiFactorAuthRecipe = require("../lib/build/recipe/multifactorauth/recipe").default; const UserRolesRecipe = require("../lib/build/recipe/userroles/recipe").default; +const OAuth2Recipe = require("../lib/build/recipe/oauth2provider/recipe").default; let { ProcessState } = require("../lib/build/processState"); let { Querier } = require("../lib/build/querier"); let { maxVersion } = require("../lib/build/utils"); @@ -266,6 +267,7 @@ module.exports.resetAll = function (disableLogging = true) { MultitenancyRecipe.reset(); TotpRecipe.reset(); MultiFactorAuthRecipe.reset(); + OAuth2Recipe.reset(); if (disableLogging) { debug.disable(); } diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index f16582249..b9be6ec15 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -11,6 +11,7 @@ import NextJS from "../../nextjs"; import ThirdParty from "../../recipe/thirdparty"; import Multitenancy from "../../recipe/multitenancy"; import Passwordless from "../../recipe/passwordless"; +import OpenId from "../../recipe/openid"; import { SMTPService as SMTPServiceTPP } from "../../recipe/passwordless/emaildelivery"; import { SMTPService as SMTPServiceP } from "../../recipe/passwordless/emaildelivery"; import { SMTPService as SMTPServiceTPEP } from "../../recipe/emailpassword/emaildelivery"; @@ -1348,14 +1349,33 @@ EmailPassword.init({ signInPOST: async (input) => { let formFields = input.formFields; let options = input.options; - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; + const emailAsUnknown = formFields.filter((f) => f.id === "email")[0].value; + const passwordAsUnknown = formFields.filter((f) => f.id === "password")[0].value; + + // NOTE: Following checks will likely never throw an error as the + // check for type is done in a parent function but they are kept + // here to be on the safe side. + if (typeof emailAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "email value needs to be a string", + }; + + if (typeof passwordAsUnknown !== "string") + return { + status: "GENERAL_ERROR", + message: "password value needs to be a string", + }; + + let email: string = emailAsUnknown; + let password: string = passwordAsUnknown; let response = await options.recipeImplementation.signIn({ email, password, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: false, userContext: input.userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { @@ -1587,16 +1607,28 @@ Session.init({ }); }, }), - openIdFeature: { - functions: (oI) => ({ - ...oI, - getOpenIdDiscoveryConfiguration: async (input) => ({ - issuer: "your issuer", - jwks_uri: "https://your.api.domain/auth/jwt/jwks.json", - status: "OK", - }), + }, +}); + +OpenId.init({ + override: { + functions: (oI) => ({ + ...oI, + getOpenIdDiscoveryConfiguration: async (input) => ({ + issuer: "your issuer", + jwks_uri: "https://your.api.domain/auth/jwt/jwks.json", + token_endpoint: "http://localhost:3000/auth/oauth2/token", + authorization_endpoint: "http://localhost:3000/auth/oauth2/auth", + userinfo_endpoint: "http://localhost:3000/auth/oauth2/userinfo", + revocation_endpoint: "http://localhost:3000/auth/oauth2/revoke", + token_introspection_endpoint: "http://localhost:3000/auth/oauth2/introspect", + end_session_endpoint: "http://localhost:3000/auth/oauth2/introspect", + id_token_signing_alg_values_supported: [], + response_types_supported: [], + subject_types_supported: [], + status: "OK", }), - }, + }), }, }); @@ -1858,21 +1890,17 @@ Passwordless.init({ const recipeUserId = new Supertokens.RecipeUserId("asdf"); -Session.init({ +JWT.init({ override: { - openIdFeature: { - jwtFeature: { - apis: (oI) => { - return { - ...oI, - getJWKSGET: async function (input) { - let result = await oI.getJWKSGET!(input); - input.options.res.setHeader("custom-header", "custom-value", false); - return result; - }, - }; + apis: (oI) => { + return { + ...oI, + getJWKSGET: async function (input) { + let result = await oI.getJWKSGET!(input); + input.options.res.setHeader("custom-header", "custom-value", false); + return result; }, - }, + }; }, }, });