├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ ├── conventional-commits-lint.js │ ├── conventional-commits.yml │ ├── docs.yml │ ├── dogfooding.yml │ ├── manual-publish.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── example └── react │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.css │ ├── App.js │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── tailwind.output.css ├── infra ├── db │ └── 00-schema.sql └── docker-compose.yml ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── AuthAdminApi.ts ├── AuthClient.ts ├── GoTrueAdminApi.ts ├── GoTrueClient.ts ├── index.ts └── lib │ ├── base64url.ts │ ├── constants.ts │ ├── error-codes.ts │ ├── errors.ts │ ├── fetch.ts │ ├── helpers.ts │ ├── local-storage.ts │ ├── locks.ts │ ├── polyfills.ts │ ├── types.ts │ └── version.ts ├── test ├── GoTrueApi.test.ts ├── GoTrueClient.test.ts ├── README.md ├── base64url.test.ts ├── fetch.test.ts ├── helpers.test.ts └── lib │ ├── clients.ts │ ├── local-storage.test.ts │ ├── locks.test.ts │ ├── polyfills.test.ts │ ├── utils.test.ts │ └── utils.ts ├── tsconfig.json └── tsconfig.module.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "ignorePatterns": ["*.md", "test/__snapshots__/**/*"], 14 | "rules": { 15 | "comma-dangle": "off", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/ban-types": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - next 9 | - rc 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | name: Test / OS ${{ matrix.os }} / Node ${{ matrix.node }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node: ["18", "20"] 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - name: Build 31 | run: | 32 | npm ci 33 | npm run build 34 | 35 | - name: Run tests 36 | run: | 37 | npm t 38 | 39 | - name: Upload coverage results to Coveralls 40 | uses: coverallsapp/github-action@master 41 | with: 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | path-to-lcov: ./test/coverage/lcov.info 44 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits-lint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | 5 | const TITLE_PATTERN = 6 | /^(?[^:!(]+)(?\([^)]+\))?(?[!])?:.+$/; 7 | const RELEASE_AS_DIRECTIVE = /^\s*Release-As:/im; 8 | const BREAKING_CHANGE_DIRECTIVE = /^\s*BREAKING[ \t]+CHANGE:/im; 9 | 10 | const ALLOWED_CONVENTIONAL_COMMIT_PREFIXES = [ 11 | "feat", 12 | "fix", 13 | "ci", 14 | "docs", 15 | "chore", 16 | ]; 17 | 18 | const object = process.argv[2]; 19 | const payload = JSON.parse(fs.readFileSync(process.argv[3], "utf-8")); 20 | 21 | let validate = []; 22 | 23 | if (object === "pr") { 24 | validate.push({ 25 | title: payload.pull_request.title, 26 | content: payload.pull_request.body, 27 | }); 28 | } else if (object === "push") { 29 | validate.push( 30 | ...payload.commits.map((commit) => ({ 31 | title: commit.message.split("\n")[0], 32 | content: commit.message, 33 | })), 34 | ); 35 | } else { 36 | console.error( 37 | `Unknown object for first argument "${object}", use 'pr' or 'push'.`, 38 | ); 39 | process.exit(0); 40 | } 41 | 42 | let failed = false; 43 | 44 | validate.forEach((payload) => { 45 | if (payload.title) { 46 | const { groups } = payload.title.match(TITLE_PATTERN); 47 | 48 | if (groups) { 49 | if (groups.breaking) { 50 | console.error( 51 | `PRs are not allowed to declare breaking changes at this stage of the project. Please remove the ! in your PR title or commit message and adjust the functionality to be backward compatible.`, 52 | ); 53 | failed = true; 54 | } 55 | 56 | if ( 57 | !ALLOWED_CONVENTIONAL_COMMIT_PREFIXES.find( 58 | (prefix) => prefix === groups.prefix, 59 | ) 60 | ) { 61 | console.error( 62 | `PR (or a commit in it) is using a disallowed conventional commit prefix ("${groups.prefix}"). Only ${ALLOWED_CONVENTIONAL_COMMIT_PREFIXES.join(", ")} are allowed. Make sure the prefix is lowercase!`, 63 | ); 64 | failed = true; 65 | } 66 | 67 | if (groups.package && groups.prefix !== "chore") { 68 | console.warn( 69 | "Avoid using package specifications in PR titles or commits except for the `chore` prefix.", 70 | ); 71 | } 72 | } else { 73 | console.error( 74 | "PR or commit title must match conventional commit structure.", 75 | ); 76 | failed = true; 77 | } 78 | } 79 | 80 | if (payload.content) { 81 | if (payload.content.match(RELEASE_AS_DIRECTIVE)) { 82 | console.error( 83 | "PR descriptions or commit messages must not contain Release-As conventional commit directives.", 84 | ); 85 | failed = true; 86 | } 87 | 88 | if (payload.content.match(BREAKING_CHANGE_DIRECTIVE)) { 89 | console.error( 90 | "PR descriptions or commit messages must not contain a BREAKING CHANGE conventional commit directive. Please adjust the functionality to be backward compatible.", 91 | ); 92 | failed = true; 93 | } 94 | } 95 | }); 96 | 97 | if (failed) { 98 | process.exit(1); 99 | } 100 | 101 | process.exit(0); 102 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | name: Check pull requests 2 | 3 | on: 4 | push: 5 | branches-ignore: # Run the checks on all branches but the protected ones 6 | - master 7 | - release/* 8 | 9 | pull_request_target: 10 | branches: 11 | - master 12 | - release/* 13 | types: 14 | - opened 15 | - edited 16 | - reopened 17 | - ready_for_review 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | check-conventional-commits: 24 | runs-on: ubuntu-latest 25 | if: github.actor != 'dependabot[bot]' # skip for dependabot PRs 26 | env: 27 | EVENT: ${{ toJSON(github.event) }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | sparse-checkout: | 32 | .github 33 | 34 | - if: ${{ github.event_name == 'pull_request_target' }} 35 | run: | 36 | set -ex 37 | TMP_FILE=$(mktemp) 38 | echo "${EVENT}" > "$TMP_FILE" 39 | node .github/workflows/conventional-commits-lint.js pr "${TMP_FILE}" 40 | 41 | - if: ${{ github.event_name == 'push' }} 42 | run: | 43 | set -ex 44 | 45 | TMP_FILE=$(mktemp) 46 | echo "${EVENT}" > "$TMP_FILE" 47 | node .github/workflows/conventional-commits-lint.js push "${TMP_FILE}" 48 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | docs: 11 | name: Publish docs / OS ${{ matrix.os }} / Node ${{ matrix.node }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | node: ['20'] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Node 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node }} 26 | 27 | - run: | 28 | npm ci 29 | npm run docs 30 | npm run docs:json 31 | 32 | - name: Publish 33 | uses: peaceiris/actions-gh-pages@v3 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | publish_dir: docs 37 | force_orphan: true 38 | commit_message: 'docs: update' 39 | -------------------------------------------------------------------------------- /.github/workflows/dogfooding.yml: -------------------------------------------------------------------------------- 1 | name: Dogfooding Check 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted, edited] 6 | 7 | pull_request: 8 | 9 | push: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | check_dogfooding: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | if: github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.ref == 'release-please--branches--master' 20 | with: 21 | ref: master # used to identify the latest RC version via git describe --tags --match rc* 22 | fetch-depth: 0 23 | 24 | - if: github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.ref == 'release-please--branches--master' 25 | run: | 26 | set -ex 27 | 28 | # finds the latest RC version on master 29 | RELEASE_VERSION=@supabase/auth-js@$(node -e "const a = '$(git describe --tags --match rc*)'.replace(/^rc/, '').split('-'); console.log(a[0] + '-' + a[1]);") 30 | 31 | # use some clever Ruby magic to extract the snapshots['@supabase/auth-js@...'] version from the pnpm-lock.yaml file 32 | STUDIO_VERSION=$(curl 'https://raw.githubusercontent.com/supabase/supabase/refs/heads/master/pnpm-lock.yaml' | ruby -e 'require("yaml"); l = YAML.load(STDIN); puts(l["snapshots"].find { |k, v| k.start_with? "@supabase/auth-js" }.first)') 33 | 34 | echo "Expecting RC version $RELEASE_VERSION to be used in Supabase Studio." 35 | 36 | if [ "$STUDIO_VERSION" != "$RELEASE_VERSION" ] 37 | then 38 | echo "Version in Supabase Studio is not the latest release candidate. Please release this RC first to proof the release before merging this PR." 39 | exit 1 40 | fi 41 | 42 | echo "Release away!" 43 | exit 0 44 | 45 | - if: github.event.pull_request.base.ref != 'master' || github.event.pull_request.head.ref != 'release-please--branches--master' 46 | run: | 47 | set -ex 48 | 49 | echo "This PR is not subject to dogfooding checks." 50 | exit 0 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manual Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | package: 7 | description: Package name 8 | type: choice 9 | required: true 10 | options: 11 | - auth-js 12 | - gotrue-js 13 | version: 14 | description: Version to publish (1.2.3 not v1.2.3) 15 | type: string 16 | required: true 17 | reason: 18 | description: Why are you manually publishing? 19 | type: string 20 | required: true 21 | 22 | jobs: 23 | publish: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - run: | 28 | echo 'Package: ${{ inputs.package }}' 29 | echo 'Version: ${{ inputs.version }}' 30 | echo 'Reason: ${{ inputs.reason }}' 31 | 32 | - uses: actions/checkout@v4 33 | with: 34 | ref: v${{ inputs.version }} 35 | 36 | - name: Set up Node 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: '20' 40 | 41 | - run: | 42 | npm ci 43 | npm run build 44 | 45 | - name: Publish @supabase/${{ inputs.package }} @v${{ inputs.version }} 46 | env: 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | run: | 49 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc 50 | 51 | set -ex 52 | 53 | for f in package.json package-lock.json 54 | do 55 | sed -i 's/0.0.0/${{ inputs.version }}/' "$f" 56 | 57 | sed -i 's|\(["/]\)auth-js|\1${{ inputs.package }}|g' "$f" 58 | done 59 | 60 | npm publish # not with --tag latest! 61 | 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | 9 | jobs: 10 | release_please: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | contents: write 15 | pull-requests: write 16 | steps: 17 | - uses: google-github-actions/release-please-action@v4 18 | id: release 19 | with: 20 | release-type: go 21 | target-branch: ${{ github.ref_name }} 22 | 23 | - uses: actions/checkout@v4 24 | if: ${{ steps.release.outputs.release_created == 'true' || steps.release.outputs.prs_created == 'true' }} 25 | with: 26 | fetch-depth: 0 27 | 28 | - if: ${{ steps.release.outputs }} 29 | id: versions 30 | run: | 31 | set -ex 32 | 33 | RELEASE_CANDIDATE=true 34 | NOT_RELEASE_CANDIDATE='${{ steps.release.outputs.release_created }}' 35 | if [ "$NOT_RELEASE_CANDIDATE" == "true" ] 36 | then 37 | RELEASE_CANDIDATE=false 38 | fi 39 | 40 | MAIN_RELEASE_VERSION=x 41 | RELEASE_VERSION=y 42 | 43 | if [ "$RELEASE_CANDIDATE" == "true" ] 44 | then 45 | # Release please doesn't tell you the candidate version when it 46 | # creates the PR, so we have to take it from the title. 47 | MAIN_RELEASE_VERSION=$(node -e "console.log('${{ steps.release.outputs.pr && fromJSON(steps.release.outputs.pr).title }}'.split(' ').reverse().find(x => x.match(/[0-9]+[.][0-9]+[.][0-9]+/)))") 48 | 49 | # Use git describe tags to identify the number of commits the branch 50 | # is ahead of the most recent non-release-candidate tag, which is 51 | # part of the rc. value. 52 | RELEASE_VERSION=$MAIN_RELEASE_VERSION-rc.$(node -e "console.log('$(git describe --tags --exclude rc*)'.split('-')[1])") 53 | 54 | # release-please only ignores releases that have a form like [A-Z0-9], so prefixing with rc 55 | RELEASE_NAME="rc$RELEASE_VERSION" 56 | else 57 | MAIN_RELEASE_VERSION=${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} 58 | RELEASE_VERSION="$MAIN_RELEASE_VERSION" 59 | RELEASE_NAME="v$RELEASE_VERSION" 60 | fi 61 | 62 | echo "MAIN_RELEASE_VERSION=${MAIN_RELEASE_VERSION}" >> "${GITHUB_ENV}" 63 | echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_ENV}" 64 | echo "RELEASE_CANDIDATE=${RELEASE_CANDIDATE}" >> "${GITHUB_ENV}" 65 | echo "RELEASE_NAME=${RELEASE_NAME}" >> "${GITHUB_ENV}" 66 | 67 | echo "MAIN_RELEASE_VERSION=${MAIN_RELEASE_VERSION}" >> "${GITHUB_OUTPUT}" 68 | echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}" 69 | echo "RELEASE_CANDIDATE=${RELEASE_CANDIDATE}" >> "${GITHUB_OUTPUT}" 70 | echo "RELEASE_NAME=${RELEASE_NAME}" >> "${GITHUB_OUTPUT}" 71 | 72 | - uses: actions/setup-node@v4 73 | if: ${{ steps.release.outputs.release_created == 'true' || steps.release.outputs.prs_created == 'true' }} 74 | with: 75 | node-version: 20 76 | 77 | - name: Build release artifacts 78 | if: ${{ steps.release.outputs.release_created == 'true' || steps.release.outputs.prs_created == 'true' }} 79 | run: | 80 | set -ex 81 | 82 | echo "export const version = '$RELEASE_VERSION'" > src/lib/version.ts 83 | 84 | npm ci 85 | npm run build 86 | 87 | for f in package.json package-lock.json 88 | do 89 | sed -i 's|"version": "0.0.0",|"version": "'"$RELEASE_VERSION"'",|g' "$f" 90 | done 91 | 92 | echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}' > ~/.npmrc 93 | 94 | DIST_TAG=patched 95 | if [ "$RELEASE_CANDIDATE" == "true" ] 96 | then 97 | DIST_TAG=rc 98 | elif [ "$GITHUB_REF" == "refs/heads/main" ] || [ "$GITHUB_REF" == "refs/heads/master" ] 99 | then 100 | # This is the main branch and it's not a prerelease, so the dist-tag should be `latest`. 101 | DIST_TAG=latest 102 | fi 103 | 104 | echo "Publishing auth-js now..." 105 | 106 | npm publish --provenance --tag "$DIST_TAG" 107 | 108 | echo "Publishing gotrue-js now..." 109 | 110 | for f in package.json package-lock.json 111 | do 112 | # only replace name not repository, homepage, etc. 113 | sed -i 's|\("name":[[:space:]]*"@supabase/\)auth-js|\1gotrue-js|g' "$f" 114 | done 115 | 116 | npm publish --provenance --tag "$DIST_TAG" 117 | 118 | - name: Create GitHub release and branches 119 | if: ${{ steps.release.outputs.release_created == 'true' || steps.release.outputs.prs_created == 'true' }} 120 | run: | 121 | set -ex 122 | 123 | if [ "$RELEASE_CANDIDATE" == "true" ] 124 | then 125 | PR_NUMBER='${{ steps.release.outputs.pr && fromJSON(steps.release.outputs.pr).number }}' 126 | 127 | GH_TOKEN='${{ github.token }}' gh release \ 128 | create $RELEASE_NAME \ 129 | --title "v$RELEASE_VERSION" \ 130 | --prerelease \ 131 | -n "This is a release candidate. See release-please PR #$PR_NUMBER for context." 132 | 133 | GH_TOKEN='${{ github.token }}' gh pr comment "$PR_NUMBER" \ 134 | -b "Release candidate [v$RELEASE_VERSION](https://github.com/supabase/gotrue-js/releases/tag/$RELEASE_NAME) published." 135 | else 136 | if [ "$GITHUB_REF" == "refs/heads/main" ] || [ "$GITHUB_REF" == "refs/heads/master" ] 137 | then 138 | IS_PATCH_ZERO=$(node -e "console.log('$RELEASE_VERSION'.endsWith('.0'))") 139 | 140 | if [ "$IS_PATCH_ZERO" == "true" ] 141 | then 142 | # Only create release branch if patch version is 0, as this 143 | # means that the release can be patched in the future. 144 | 145 | GH_TOKEN='${{ github.token }}' gh api \ 146 | --method POST \ 147 | -H "Accept: application/vnd.github+json" \ 148 | -H "X-GitHub-Api-Version: 2022-11-28" \ 149 | /repos/supabase/gotrue-js/git/refs \ 150 | -f "ref=refs/heads/release/${RELEASE_VERSION}" \ 151 | -f "sha=$GITHUB_SHA" 152 | fi 153 | fi 154 | fi 155 | 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .DS_Store 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # VisualStudioCode 108 | .vscode/ 109 | 110 | # Docs via yarn docs 111 | docs/v2 112 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { "name": "master" }, 4 | { "name": "next", "channel": "next", "prerelease": true }, 5 | { "name": "rc", "channel": "rc", "prerelease": true } 6 | ], 7 | "plugins": [ 8 | [ 9 | "semantic-release-plugin-update-version-in-files", 10 | { 11 | "files": [ 12 | "src/lib/version.ts", 13 | "dist/main/lib/version.js", 14 | "dist/main/lib/version.d.ts", 15 | "dist/module/lib/version.js", 16 | "dist/module/lib/version.d.ts" 17 | ], 18 | "placeholder": "0.0.0" 19 | } 20 | ], 21 | "@semantic-release/commit-analyzer", 22 | "@semantic-release/release-notes-generator", 23 | "@semantic-release/github", 24 | "@semantic-release/npm" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.70.0](https://github.com/supabase/auth-js/compare/v2.69.1...v2.70.0) (2025-05-16) 4 | 5 | 6 | ### Features 7 | 8 | * add `signInWithWeb3` with solana ([#1037](https://github.com/supabase/auth-js/issues/1037)) ([cff5bcb](https://github.com/supabase/auth-js/commit/cff5bcb8399a46b293cfa8688d89882924e7edab)) 9 | * validate uuid and sign out scope parameters to functions ([#1063](https://github.com/supabase/auth-js/issues/1063)) ([1bcb76e](https://github.com/supabase/auth-js/commit/1bcb76e479e51cd9bca2d7732d0bf3199e07a693)) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * add missing `deleted_at` property to `User` interface ([#1059](https://github.com/supabase/auth-js/issues/1059)) ([96da194](https://github.com/supabase/auth-js/commit/96da194b9ffb88643caa1547084fcee4dbe560f3)) 15 | * export `processLock` from toplevel ([#1057](https://github.com/supabase/auth-js/issues/1057)) ([d99695a](https://github.com/supabase/auth-js/commit/d99695af9e632178be94502255c75496cda191ad)) 16 | 17 | ## [2.69.1](https://github.com/supabase/auth-js/compare/v2.69.0...v2.69.1) (2025-03-24) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * `generatePKCEChallenge` should use btoa ([#1044](https://github.com/supabase/auth-js/issues/1044)) ([c06fafb](https://github.com/supabase/auth-js/commit/c06fafbc61fa1dec62ba0183530b6b566a135b75)) 23 | 24 | ## [2.69.0](https://github.com/supabase/auth-js/compare/v2.68.0...v2.69.0) (2025-03-22) 25 | 26 | 27 | ### Features 28 | 29 | * introduce getClaims method to verify asymmetric JWTs ([#1030](https://github.com/supabase/auth-js/issues/1030)) ([daa2669](https://github.com/supabase/auth-js/commit/daa266949b336d2e78f2a7b9c9837b70abeab7a6)) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * assert type in `decodeJWTPayload` ([#1018](https://github.com/supabase/auth-js/issues/1018)) ([3d80039](https://github.com/supabase/auth-js/commit/3d80039e8b64402b615a924ff82f6562405ff705)) 35 | * set jwks_cached_at ([#1039](https://github.com/supabase/auth-js/issues/1039)) ([9bdc023](https://github.com/supabase/auth-js/commit/9bdc0232e5939722606d22bbeaadb131f0dc2734)) 36 | 37 | ## [2.68.0](https://github.com/supabase/auth-js/compare/v2.67.3...v2.68.0) (2025-01-21) 38 | 39 | 40 | ### Features 41 | 42 | * consider session expired with margin on getSession() without auto refresh ([#1027](https://github.com/supabase/auth-js/issues/1027)) ([80f88e4](https://github.com/supabase/auth-js/commit/80f88e4bd2809db765a8d103954e827d8473b7db)) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * remove `internal-types.ts` ([#1014](https://github.com/supabase/auth-js/issues/1014)) ([28ead89](https://github.com/supabase/auth-js/commit/28ead89af47bcdaccc6cc2f2c7f013bed8cf3d50)) 48 | * update docs to add scrypt ([#1012](https://github.com/supabase/auth-js/issues/1012)) ([1225239](https://github.com/supabase/auth-js/commit/1225239e239bde1b25037a88867d4c484caf8301)) 49 | 50 | ## [2.67.3](https://github.com/supabase/auth-js/compare/v2.67.2...v2.67.3) (2024-12-17) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * return redirect errors early ([#1003](https://github.com/supabase/auth-js/issues/1003)) ([9751b80](https://github.com/supabase/auth-js/commit/9751b8029b4235a63dcb525e7ce7cc942c85daf5)) 56 | 57 | ## [2.67.2](https://github.com/supabase/auth-js/compare/v2.67.1...v2.67.2) (2024-12-16) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * `isBrowser()` to include check on `window` ([#982](https://github.com/supabase/auth-js/issues/982)) ([645f224](https://github.com/supabase/auth-js/commit/645f22447e68ba13e43e359d1524e95fe025d771)) 63 | 64 | ## [2.67.1](https://github.com/supabase/auth-js/compare/v2.67.0...v2.67.1) (2024-12-13) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * revert [#992](https://github.com/supabase/auth-js/issues/992) and [#993](https://github.com/supabase/auth-js/issues/993) ([#999](https://github.com/supabase/auth-js/issues/999)) ([12b2848](https://github.com/supabase/auth-js/commit/12b2848237854f3d70b9989920ad50e2c4186fff)) 70 | 71 | ## [2.67.0](https://github.com/supabase/auth-js/compare/v2.66.1...v2.67.0) (2024-12-12) 72 | 73 | 74 | ### Features 75 | 76 | * wrap navigator.locks.request with plain promise to help zone.js ([#989](https://github.com/supabase/auth-js/issues/989)) ([2e6e07c](https://github.com/supabase/auth-js/commit/2e6e07c21a561ca13d5e74b69609c2cc93f104f4)), closes [#830](https://github.com/supabase/auth-js/issues/830) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * add email_address_invalid error code ([#994](https://github.com/supabase/auth-js/issues/994)) ([232f133](https://github.com/supabase/auth-js/commit/232f133b1a84b4c667e994f472098aa5cde2088d)) 82 | * return error early for redirects ([#992](https://github.com/supabase/auth-js/issues/992)) ([9f32d30](https://github.com/supabase/auth-js/commit/9f32d30e17954c5d4320b374a108617cda5ab357)) 83 | 84 | ## [2.66.1](https://github.com/supabase/auth-js/compare/v2.66.0...v2.66.1) (2024-12-04) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * add loose auto complete to string literals where applicable ([#966](https://github.com/supabase/auth-js/issues/966)) ([fd9248d](https://github.com/supabase/auth-js/commit/fd9248d7aecd0bd00381dff162969d8014a3359a)) 90 | * add new error codes ([#979](https://github.com/supabase/auth-js/issues/979)) ([dfb40d2](https://github.com/supabase/auth-js/commit/dfb40d24188f7e8b0d34e51ded15582086250c51)) 91 | * don't remove session for identity linking errors ([#987](https://github.com/supabase/auth-js/issues/987)) ([e68ebe6](https://github.com/supabase/auth-js/commit/e68ebe604d15d881b23678d180cccb7115f16f4e)) 92 | 93 | ## [2.66.0](https://github.com/supabase/auth-js/compare/v2.65.1...v2.66.0) (2024-11-01) 94 | 95 | 96 | ### Features 97 | 98 | * add process lock for optional use in non-browser environments (React Native) ([#977](https://github.com/supabase/auth-js/issues/977)) ([8af88b6](https://github.com/supabase/auth-js/commit/8af88b6f4e41872b73e84c40f71793dab6c62126)) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * typo in warning message ([#975](https://github.com/supabase/auth-js/issues/975)) ([4f21f93](https://github.com/supabase/auth-js/commit/4f21f9324b2c3d55630b8d0a6759a264b0472dd8)) 104 | * update soft-deletion docs ([#973](https://github.com/supabase/auth-js/issues/973)) ([cb052a9](https://github.com/supabase/auth-js/commit/cb052a9b0846048feef18080d830cc36a9ed7282)) 105 | 106 | ## [2.65.1](https://github.com/supabase/auth-js/compare/v2.65.0...v2.65.1) (2024-10-14) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * Call `SIGNED_OUT` event where session is removed ([#854](https://github.com/supabase/auth-js/issues/854)) ([436fd9f](https://github.com/supabase/auth-js/commit/436fd9f967ad6d515b8eca179d06032619a1b071)) 112 | * improve `mfa.enroll` return types ([#956](https://github.com/supabase/auth-js/issues/956)) ([8a1ec06](https://github.com/supabase/auth-js/commit/8a1ec0602792191bd235d51fd45c0ec2cabdf216)) 113 | * move MFA sub types to internal file ([#964](https://github.com/supabase/auth-js/issues/964)) ([4b7455c](https://github.com/supabase/auth-js/commit/4b7455c2631ca4e00f01275c7342eb37756ede23)) 114 | * remove phone mfa deletion, match on error codes ([#963](https://github.com/supabase/auth-js/issues/963)) ([ef3911c](https://github.com/supabase/auth-js/commit/ef3911cd1a082a6825ce25fe326081e096bd55f5)) 115 | 116 | ## [2.65.0](https://github.com/supabase/auth-js/compare/v2.64.4...v2.65.0) (2024-08-27) 117 | 118 | 119 | ### Features 120 | 121 | * add bindings for Multi-Factor Authentication (Phone) ([#932](https://github.com/supabase/auth-js/issues/932)) ([b957c30](https://github.com/supabase/auth-js/commit/b957c30782065e4cc421a526c62c101d35c443d4)) 122 | * add kakao to sign in with ID token ([#845](https://github.com/supabase/auth-js/issues/845)) ([e2337ba](https://github.com/supabase/auth-js/commit/e2337bad535598d9f751505de52a18c59f1505c3)) 123 | * remove session, emit `SIGNED_OUT` when JWT `session_id` is invalid ([#905](https://github.com/supabase/auth-js/issues/905)) ([db41710](https://github.com/supabase/auth-js/commit/db41710b1a35ef559158a936d0a95acc0b1fca96)) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * Correct typo in GoTrueClient warning message ([#938](https://github.com/supabase/auth-js/issues/938)) ([8222ee1](https://github.com/supabase/auth-js/commit/8222ee198a0ab10570e8b4c31ffb2aeafef86392)) 129 | * don't throw error in exchangeCodeForSession ([#946](https://github.com/supabase/auth-js/issues/946)) ([6e161ec](https://github.com/supabase/auth-js/commit/6e161ece3f8cd0d115857e2ed4346533840769f0)) 130 | * move docker compose to v2 ([#940](https://github.com/supabase/auth-js/issues/940)) ([38eef89](https://github.com/supabase/auth-js/commit/38eef89ff61b49eb65ee26b7d2201148d1fc3b77)) 131 | 132 | ## [2.64.4](https://github.com/supabase/auth-js/compare/v2.64.3...v2.64.4) (2024-07-12) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * update types ([#930](https://github.com/supabase/auth-js/issues/930)) ([dbc5962](https://github.com/supabase/auth-js/commit/dbc5962d609cc0470b5b03160f4cd8b9e7d03ce3)) 138 | 139 | ## [2.64.3](https://github.com/supabase/auth-js/compare/v2.64.2...v2.64.3) (2024-06-17) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * don't call removeSession prematurely ([#915](https://github.com/supabase/auth-js/issues/915)) ([e0dc518](https://github.com/supabase/auth-js/commit/e0dc51849680fa8f1900de786c4a7e77eab8760e)) 145 | * limit proxy session warning to once per client instance ([#900](https://github.com/supabase/auth-js/issues/900)) ([4ecfdda](https://github.com/supabase/auth-js/commit/4ecfdda65188b71322753e57622be8eafe97ed6b)) 146 | * patch release workflow ([#922](https://github.com/supabase/auth-js/issues/922)) ([f84fb50](https://github.com/supabase/auth-js/commit/f84fb50a4357af49acac6ca151057d2af74d63c9)) 147 | * type errors in verifyOtp ([#918](https://github.com/supabase/auth-js/issues/918)) ([dcd0b9b](https://github.com/supabase/auth-js/commit/dcd0b9b682412a2f1d2deaab26eb8094e50b67fd)) 148 | 149 | ## [2.64.2](https://github.com/supabase/auth-js/compare/v2.64.1...v2.64.2) (2024-05-03) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * signOut should ignore 403s ([#894](https://github.com/supabase/auth-js/issues/894)) ([eeb77ce](https://github.com/supabase/auth-js/commit/eeb77ce2a1ddee94c38f17533c9b748bf2950f67)) 155 | * suppress getSession warning whenever _saveSession is called ([#895](https://github.com/supabase/auth-js/issues/895)) ([59ec9af](https://github.com/supabase/auth-js/commit/59ec9affa01c780fb18f668291fa7167a65c391d)) 156 | 157 | ## [2.64.1](https://github.com/supabase/auth-js/compare/v2.64.0...v2.64.1) (2024-04-25) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * return error if missing session or missing custom auth header ([#891](https://github.com/supabase/auth-js/issues/891)) ([8d16578](https://github.com/supabase/auth-js/commit/8d165787ec46929cba68d18c35161463240f61e3)) 163 | 164 | ## [2.64.0](https://github.com/supabase/auth-js/compare/v2.63.2...v2.64.0) (2024-04-25) 165 | 166 | 167 | ### Features 168 | 169 | * remove `cache: no-store` as it breaks cloudflare ([#886](https://github.com/supabase/auth-js/issues/886)) ([10e9d38](https://github.com/supabase/auth-js/commit/10e9d3871c5a9ce50d15c35c7fd7045cad504670)) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * Revert "fix: `getUser` returns null if there is no session ([#876](https://github.com/supabase/auth-js/issues/876))" ([#889](https://github.com/supabase/auth-js/issues/889)) ([6755fef](https://github.com/supabase/auth-js/commit/6755fef2aefd1bc84a26182f848c0912492cb106)) 175 | * revert check for access token in header ([#885](https://github.com/supabase/auth-js/issues/885)) ([03d8ba7](https://github.com/supabase/auth-js/commit/03d8ba7ca5c485979788d6f121199e4370622491)) 176 | 177 | ## [2.63.2](https://github.com/supabase/auth-js/compare/v2.63.1...v2.63.2) (2024-04-20) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * check for access token in header ([#882](https://github.com/supabase/auth-js/issues/882)) ([ae4a53d](https://github.com/supabase/auth-js/commit/ae4a53de7eb41ebde3b4e1abe823e2ffcb53a71d)) 183 | 184 | ## [2.63.1](https://github.com/supabase/auth-js/compare/v2.63.0...v2.63.1) (2024-04-18) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * `getUser` returns null if there is no session ([#876](https://github.com/supabase/auth-js/issues/876)) ([6adf8ca](https://github.com/supabase/auth-js/commit/6adf8caa4ca803e65f943cc88a2849f5905a044a)) 190 | * implement exponential back off on the retries of `_refreshAccessToken` method ([#869](https://github.com/supabase/auth-js/issues/869)) ([f66711d](https://github.com/supabase/auth-js/commit/f66711ddf87ea705a972a860d7ebfb6e0d003c6b)) 191 | * update session warning ([#879](https://github.com/supabase/auth-js/issues/879)) ([3661130](https://github.com/supabase/auth-js/commit/36611300fa6d1378a7633c62d2f816d3803f2774)) 192 | 193 | ## [2.63.0](https://github.com/supabase/gotrue-js/compare/v2.62.2...v2.63.0) (2024-03-26) 194 | 195 | 196 | ### Features 197 | 198 | * add method for anonymous sign-in ([#858](https://github.com/supabase/gotrue-js/issues/858)) ([e8a1fc9](https://github.com/supabase/gotrue-js/commit/e8a1fc9a40947b949080107138eade09f06f5868)) 199 | * add support for error codes ([#855](https://github.com/supabase/gotrue-js/issues/855)) ([99821f4](https://github.com/supabase/gotrue-js/commit/99821f4a1f6fdb3a222cd0f660210016e6cc823e)) 200 | * explicit `cache: no-store` in fetch ([#847](https://github.com/supabase/gotrue-js/issues/847)) ([034bee0](https://github.com/supabase/gotrue-js/commit/034bee09c3f0a4613d9a3e7bd3bc5f70682f5a66)) 201 | * warn use of `getSession()` when `isServer` on storage ([#846](https://github.com/supabase/gotrue-js/issues/846)) ([9ea94fe](https://github.com/supabase/gotrue-js/commit/9ea94fe11f4a6a4b6305aa4fe75c4661074437a7)) 202 | 203 | 204 | ### Bug Fixes 205 | 206 | * refactor all pkce code into a single method ([#860](https://github.com/supabase/gotrue-js/issues/860)) ([860bffc](https://github.com/supabase/gotrue-js/commit/860bffc8f75292e71630fb7241e11a754200dab8)) 207 | * remove data type ([#848](https://github.com/supabase/gotrue-js/issues/848)) ([15c7c82](https://github.com/supabase/gotrue-js/commit/15c7c8258b2d42d3378be4f7738c728a07523579)) 208 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Supabase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `auth-js` 2 | 3 | An isomorphic JavaScript client library for the [Supabase Auth](https://github.com/supabase/auth) API. 4 | 5 | ## Docs 6 | 7 | - Using `auth-js`: https://supabase.com/docs/reference/javascript/auth-signup 8 | - TypeDoc: https://supabase.github.io/auth-js/v2 9 | 10 | ## Quick start 11 | 12 | Install 13 | 14 | ```bash 15 | npm install --save @supabase/auth-js 16 | ``` 17 | 18 | Usage 19 | 20 | ```js 21 | import { AuthClient } from '@supabase/auth-js' 22 | 23 | const GOTRUE_URL = 'http://localhost:9999' 24 | 25 | const auth = new AuthClient({ url: GOTRUE_URL }) 26 | ``` 27 | 28 | - `signUp()`: https://supabase.io/docs/reference/javascript/auth-signup 29 | - `signIn()`: https://supabase.io/docs/reference/javascript/auth-signin 30 | - `signOut()`: https://supabase.io/docs/reference/javascript/auth-signout 31 | 32 | ### Custom `fetch` implementation 33 | 34 | `auth-js` uses the [`cross-fetch`](https://www.npmjs.com/package/cross-fetch) library to make HTTP requests, but an alternative `fetch` implementation can be provided as an option. This is most useful in environments where `cross-fetch` is not compatible, for instance Cloudflare Workers: 35 | 36 | ```js 37 | import { AuthClient } from '@supabase/auth-js' 38 | 39 | const AUTH_URL = 'http://localhost:9999' 40 | 41 | const auth = new AuthClient({ url: AUTH_URL, fetch: fetch }) 42 | ``` 43 | 44 | ## Sponsors 45 | 46 | We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. 47 | 48 | [![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) 49 | 50 | ![Watch this repo](https://gitcdn.xyz/repo/supabase/monorepo/master/web/static/watch-repo.gif 'Watch this repo') 51 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | Releases are handled by Semantic release. This document is for forcing and documenting any non-code changes. 4 | 5 | ### v1.27 6 | 7 | - Fix: https://github.com/supabase/gotrue-js/pull/184 8 | 9 | ### v1.12.7 10 | 11 | - Fix https://github.com/supabase/gotrue-js/issues/73 12 | 13 | ### v1.12.0 14 | 15 | - Feature: https://github.com/supabase/gotrue-js/pull/66 OAuth providers can now be supplied scopes. 16 | 17 | ### v1.11.0 18 | 19 | - Feature: https://github.com/supabase/gotrue-js/issues/62 Give the ability for developers to redirect their users to a specified URL after they are logged in. 20 | 21 | ### v1.10.1 22 | 23 | - Fix https://github.com/supabase/gotrue-js/issues/38 24 | - Fix https://github.com/supabase/gotrue-js/issues/41 25 | 26 | ### v1.7.3 27 | 28 | Fixes React Native error. From @tjg1: https://github.com/supabase/gotrue-js/pull/26 29 | 30 | ### v1.7.2 31 | 32 | Adds the types, provided by @duncanhealy: https://github.com/supabase/gotrue-js/pull/24 33 | 34 | ### v1.7.0 35 | 36 | In this release we added `client.api.sendMagicLinkEmail()` and updated `client.signIn()` to support magic link login by only providing email credentials without a password. 37 | 38 | ### v1.6.1 39 | 40 | In this release we strip out the session data from the URL once it is detected. 41 | 42 | ### v1.6.0 43 | 44 | In this release we added `client.user()`, `client.session()`, and `client.refreshSession()`. 45 | 46 | ### v1.5.10 47 | 48 | In this one we had to roll back the automatic Semantic release, which bumped the version to 2.0.0. 49 | 50 | This release containes some breaking changes: 51 | 52 | ```js 53 | // previously 54 | let client = new Client() 55 | 56 | // this release 57 | let client = new GoTrueClient() 58 | ``` 59 | -------------------------------------------------------------------------------- /example/react/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_SUPABASE_URL=http://localhost:9998 2 | REACT_APP_SUPABASE_ANON_KEY=supabase_anon_key -------------------------------------------------------------------------------- /example/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/react/README.md: -------------------------------------------------------------------------------- 1 | # Supabase `gotrue-js` example 2 | 3 | ## Setup 4 | 5 | ### With a Supabase project 6 | 7 | ### 1. Create new project 8 | 9 | Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start. 10 | 11 | ### 2. Get the URL and Key 12 | 13 | Create a copy of `.env.local.example`: 14 | 15 | ```bash 16 | cp .env.local.example .env.local 17 | ``` 18 | 19 | Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key and set them in your newly created `.env.local` file. 20 | 21 | #### [Optional] - Set up OAuth providers 22 | 23 | You can use third-party login providers like GitHub or Google. Refer to the [docs](https://supabase.io/docs/guides/auth#third-party-logins) to learn how to configure these. 24 | 25 | ### 3. Install and run 26 | 27 | ```bash 28 | npm install 29 | npm run dev 30 | # or 31 | yarn 32 | yarn dev 33 | ``` 34 | 35 | ### Docker 36 | 37 | You can find a docker compose file for spinning up a [Gotrue server](https://github.com/supabase/gotrue) in the [infra folder](../infra). 38 | 39 | ## Run 40 | 41 | ### Install root dependencies and build `gotrue-js` 42 | 43 | ```bash 44 | cd -; npm install; npm run build; cd example 45 | ``` 46 | 47 | ### Install the dependencies and run the example 48 | 49 | ```bash 50 | npm install; npm run start 51 | ``` 52 | 53 |
54 | 55 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 56 | 57 | ## Available Scripts 58 | 59 | In the project directory, you can run: 60 | 61 | ### `npm start` 62 | 63 | Runs the app in the development mode.
64 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 65 | 66 | The page will reload if you make edits.
67 | You will also see any lint errors in the console. 68 | 69 | ### `npm test` 70 | 71 | Launches the test runner in the interactive watch mode.
72 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 73 | 74 | ### `npm run build` 75 | 76 | Builds the app for production to the `build` folder.
77 | It correctly bundles React in production mode and optimizes the build for the best performance. 78 | 79 | The build is minified and the filenames include the hashes.
80 | Your app is ready to be deployed! 81 | 82 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 83 | 84 | ### `npm run eject` 85 | 86 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 87 | 88 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 89 | 90 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 91 | 92 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 93 | 94 | ## Learn More 95 | 96 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 97 | 98 | To learn React, check out the [React documentation](https://reactjs.org/). 99 | 100 | ### Code Splitting 101 | 102 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 103 | 104 | ### Analyzing the Bundle Size 105 | 106 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 107 | 108 | ### Making a Progressive Web App 109 | 110 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 111 | 112 | ### Advanced Configuration 113 | 114 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 115 | 116 | ### Deployment 117 | 118 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 119 | 120 | ### `npm run build` fails to minify 121 | 122 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 123 | -------------------------------------------------------------------------------- /example/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@supabase/auth-js": "file:../..", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "5.0.1", 13 | "tailwindcss": "^1.8.10" 14 | }, 15 | "devDependencies": { 16 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 17 | }, 18 | "scripts": { 19 | "build:tailwind": "tailwindcss build src/App.css -o src/tailwind.output.css", 20 | "prestart": "npm run build:tailwind", 21 | "prebuild": "npm run build:tailwind", 22 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 23 | "build": "react-scripts build", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/auth-js/e7b2f2169cbbf2cd1e56526c488fc7c169335eac/example/react/public/favicon.ico -------------------------------------------------------------------------------- /example/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/auth-js/e7b2f2169cbbf2cd1e56526c488fc7c169335eac/example/react/public/logo192.png -------------------------------------------------------------------------------- /example/react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/auth-js/e7b2f2169cbbf2cd1e56526c488fc7c169335eac/example/react/public/logo512.png -------------------------------------------------------------------------------- /example/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/react/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; -------------------------------------------------------------------------------- /example/react/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import { AuthClient } from '@supabase/auth-js' 3 | import './tailwind.output.css' 4 | 5 | const supabaseURL = process.env.REACT_APP_SUPABASE_URL 6 | const supabaseAnon = process.env.REACT_APP_SUPABASE_ANON_KEY 7 | 8 | const auth = new AuthClient({ 9 | url: `${supabaseURL}/auth/v1`, 10 | headers: { 11 | accept: 'json', 12 | apikey: supabaseAnon, 13 | }, 14 | }) 15 | 16 | function App() { 17 | let [session, setSession] = useState() 18 | let [email, setEmail] = useState(localStorage.getItem('email') ?? '') 19 | let [phone, setPhone] = useState(localStorage.getItem('phone') ?? '') 20 | let [password, setPassword] = useState('') 21 | let [otp, setOtp] = useState('') 22 | let [rememberMe, setRememberMe] = useState(false) 23 | 24 | const modalRef = useRef(null) 25 | let [showModal, setShowModal] = useState(false) 26 | 27 | async function getSession() { 28 | const { data, error } = await auth.getSession() 29 | if (error | !data) { 30 | setSession('') 31 | } else { 32 | setSession(data.session) 33 | } 34 | } 35 | useEffect(() => { 36 | getSession() 37 | }, []) 38 | 39 | useEffect(() => { 40 | let { data: subscription } = auth.onAuthStateChange((event, session) => { 41 | console.log(event, session) 42 | if (event === 'SIGNED_OUT' || event === 'SIGNED_IN') { 43 | setSession(session) 44 | } 45 | }) 46 | 47 | return () => { 48 | if (subscription.subscription) { 49 | subscription.unsubscribe() 50 | } 51 | } 52 | }, []) 53 | 54 | async function handleOAuthLogin(provider) { 55 | let { error } = await auth.signInWithOAuth({ 56 | provider, 57 | options: { 58 | redirectTo: 'http://localhost:3001/welcome', 59 | }, 60 | }) 61 | if (error) console.log('Error: ', error.message) 62 | } 63 | async function handleVerifyOtp() { 64 | await auth.verifyOTP({ phone: phone, token: otp, type: 'sms' }) 65 | } 66 | 67 | async function handleSendOtp() { 68 | await auth.signInWithOtp({ phone: phone, type: 'sms' }) 69 | } 70 | async function handleEmailSignIn() { 71 | if (rememberMe) { 72 | localStorage.setItem('email', email) 73 | } else { 74 | localStorage.removeItem('email') 75 | } 76 | 77 | let { error, data } = password 78 | ? await auth.signInWithPassword({ email, password }) 79 | : await auth.signInWithOtp({ email }) 80 | if (!error && !data) alert('Check your email for the login link!') 81 | if (error) console.log('Error: ', error.message) 82 | } 83 | async function handleEmailSignUp() { 84 | let { error } = await auth.signUp({ 85 | email, 86 | password, 87 | options: { emailRedirectTo: 'http://localhost:3001/welcome' }, 88 | }) 89 | if (error) console.log('Error: ', error.message) 90 | } 91 | async function handleSignOut() { 92 | let { error } = await auth.signOut() 93 | if (error) console.log('Error: ', error) 94 | } 95 | async function handleSignInAnonymously(data) { 96 | let { error } = await auth.signInAnonymously({ options: { data } }) 97 | if (error) alert(error.message) 98 | } 99 | async function forgotPassword() { 100 | var email = prompt('Please enter your email:') 101 | if (email === null || email === '') { 102 | window.alert('You must enter your email.') 103 | } else { 104 | let { error } = await auth.resetPasswordForEmail(email) 105 | if (error) { 106 | console.log('Error: ', error.message) 107 | } else { 108 | alert('Password recovery email has been sent.') 109 | } 110 | } 111 | } 112 | 113 | const showIdentities = () => { 114 | return session?.user?.identities?.map((identity) => { 115 | return ( 116 |
120 |
121 | {identity.provider[0].toUpperCase() + identity.provider.slice(1)} 122 |
123 |
{identity?.identity_data?.email}
124 |
125 | 132 |
133 |
134 | ) 135 | }) 136 | } 137 | 138 | const showLinkingOptions = () => { 139 | setShowModal(!showModal) 140 | if (showModal && !modalRef.current?.open) { 141 | modalRef.current?.showModal() 142 | } else { 143 | modalRef.current?.close() 144 | } 145 | } 146 | 147 | const linkingOptionsModal = () => { 148 | return ( 149 | 150 |

Continue linking with:

151 |
152 |
153 | 154 | 161 | 162 |
163 |
164 | 165 | 172 | 173 |
174 |
175 | 182 |
183 | ) 184 | } 185 | 186 | async function handleUnlinkIdentity(identity) { 187 | let { error } = await auth.unlinkIdentity(identity) 188 | if (error) { 189 | alert(error.message) 190 | } else { 191 | alert(`successfully unlinked ${identity.provider} identity`) 192 | const { data, error: refreshSessionError } = await auth.refreshSession() 193 | if (refreshSessionError) alert(refreshSessionError.message) 194 | setSession(data.session) 195 | } 196 | } 197 | return ( 198 |
199 |
200 |
201 |

Active session

202 |
206 |             {!session ? 'None' : JSON.stringify(session, null, 2)}
207 |           
208 | {session && ( 209 |
210 | 211 | 218 | 219 |
220 | )} 221 |
222 | 223 |
224 |

Identities

225 | {showIdentities()} 226 | 233 | {linkingOptionsModal()} 234 |
235 | 236 |
237 |
238 | 241 |
242 | setEmail(e.target.value)} 247 | required 248 | className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" 249 | /> 250 |
251 |
252 | 253 |
254 | 257 |
258 | setPassword(e.target.value)} 263 | required 264 | className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" 265 | /> 266 |
267 |
268 | 269 |
270 | 273 |
274 | setPhone(e.target.value)} 279 | required 280 | className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" 281 | /> 282 |
283 |
284 | 285 |
286 | 289 |
290 | setOtp(e.target.value)} 295 | required 296 | className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" 297 | /> 298 |
299 |
300 | 301 |
302 |
303 | setRememberMe(!rememberMe)} 307 | className="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out" 308 | /> 309 | 312 |
313 | 314 |
315 | {/* eslint-disable-next-line */} 316 | 321 | Forgot your password? 322 | 323 |
324 |
325 | 326 |
327 | 328 | 335 | 336 | 337 | 344 | 345 | 346 | 353 | 354 | 355 | 362 | 363 |
364 | 365 |
366 |
367 |
368 |
369 |
370 |
371 | Or continue with 372 |
373 |
374 | 375 |
376 |
377 | 378 | 385 | 386 |
387 |
388 | 389 | 396 | 397 |
398 |
399 | 400 | 407 | 408 |
409 |
410 |
411 |
412 |
413 |
414 | ) 415 | } 416 | 417 | export default App 418 | -------------------------------------------------------------------------------- /example/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /example/react/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /infra/db/00-schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION postgres; 2 | -- auth.users definition 3 | CREATE TABLE auth.users ( 4 | instance_id uuid NULL, 5 | id uuid NOT NULL, 6 | aud varchar(255) NULL, 7 | "role" varchar(255) NULL, 8 | email varchar(255) NULL, 9 | encrypted_password varchar(255) NULL, 10 | confirmed_at timestamptz NULL, 11 | invited_at timestamptz NULL, 12 | confirmation_token varchar(255) NULL, 13 | confirmation_sent_at timestamptz NULL, 14 | recovery_token varchar(255) NULL, 15 | recovery_sent_at timestamptz NULL, 16 | email_change_token varchar(255) NULL, 17 | email_change varchar(255) NULL, 18 | email_change_sent_at timestamptz NULL, 19 | last_sign_in_at timestamptz NULL, 20 | raw_app_meta_data jsonb NULL, 21 | raw_user_meta_data jsonb NULL, 22 | is_super_admin bool NULL, 23 | created_at timestamptz NULL, 24 | updated_at timestamptz NULL, 25 | CONSTRAINT users_pkey PRIMARY KEY (id) 26 | ); 27 | CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, email); 28 | CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); 29 | -- auth.refresh_tokens definition 30 | CREATE TABLE auth.refresh_tokens ( 31 | instance_id uuid NULL, 32 | id bigserial NOT NULL, 33 | "token" varchar(255) NULL, 34 | user_id varchar(255) NULL, 35 | revoked bool NULL, 36 | created_at timestamptz NULL, 37 | updated_at timestamptz NULL, 38 | CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id) 39 | ); 40 | CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); 41 | CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); 42 | CREATE INDEX refresh_tokens_token_idx ON auth.refresh_tokens USING btree (token); 43 | -- auth.instances definition 44 | CREATE TABLE auth.instances ( 45 | id uuid NOT NULL, 46 | uuid uuid NULL, 47 | raw_base_config text NULL, 48 | created_at timestamptz NULL, 49 | updated_at timestamptz NULL, 50 | CONSTRAINT instances_pkey PRIMARY KEY (id) 51 | ); 52 | -- auth.audit_log_entries definition 53 | CREATE TABLE auth.audit_log_entries ( 54 | instance_id uuid NULL, 55 | id uuid NOT NULL, 56 | payload json NULL, 57 | created_at timestamptz NULL, 58 | CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id) 59 | ); 60 | CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); 61 | -- auth.schema_migrations definition 62 | CREATE TABLE auth.schema_migrations ( 63 | "version" varchar(255) NOT NULL, 64 | CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version") 65 | ); 66 | INSERT INTO auth.schema_migrations (version) 67 | VALUES ('20171026211738'), 68 | ('20171026211808'), 69 | ('20171026211834'), 70 | ('20180103212743'), 71 | ('20180108183307'), 72 | ('20180119214651'), 73 | ('20180125194653'); 74 | -- Gets the User ID from the request cookie 75 | create or replace function auth.uid() returns uuid as $$ 76 | select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid; 77 | $$ language sql stable; 78 | -- Gets the User ID from the request cookie 79 | create or replace function auth.role() returns text as $$ 80 | select nullif(current_setting('request.jwt.claim.role', true), '')::text; 81 | $$ language sql stable; 82 | GRANT ALL PRIVILEGES ON SCHEMA auth TO postgres; 83 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO postgres; 84 | GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO postgres; 85 | ALTER USER postgres SET search_path = "auth"; 86 | 87 | -------------------------------------------------------------------------------- /infra/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3' 3 | services: 4 | gotrue: # Signup enabled, autoconfirm off 5 | image: supabase/auth:v2.151.0 6 | ports: 7 | - '9999:9999' 8 | environment: 9 | GOTRUE_MAILER_URLPATHS_CONFIRMATION: '/verify' 10 | GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' 11 | GOTRUE_JWT_EXP: 3600 12 | GOTRUE_DB_DRIVER: postgres 13 | DB_NAMESPACE: auth 14 | GOTRUE_API_HOST: 0.0.0.0 15 | PORT: 9999 16 | GOTRUE_DISABLE_SIGNUP: 'false' 17 | API_EXTERNAL_URL: http://localhost:9999 18 | GOTRUE_SITE_URL: http://localhost:9999 19 | GOTRUE_URI_ALLOW_LIST: https://supabase.io/docs 20 | GOTRUE_MAILER_AUTOCONFIRM: 'false' 21 | GOTRUE_LOG_LEVEL: DEBUG 22 | GOTRUE_OPERATOR_TOKEN: super-secret-operator-token 23 | DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' 24 | GOTRUE_EXTERNAL_GOOGLE_ENABLED: 'true' 25 | GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: 53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com 26 | GOTRUE_EXTERNAL_GOOGLE_SECRET: Sm3s8RE85rDcS36iMy8YjrpC 27 | GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: http://localhost:9999/callback 28 | GOTRUE_SMTP_HOST: mail 29 | GOTRUE_SMTP_PORT: 2500 30 | GOTRUE_SMTP_USER: GOTRUE_SMTP_USER 31 | GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS 32 | GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com 33 | GOTRUE_MAILER_SUBJECTS_CONFIRMATION: 'Please confirm' 34 | GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' 35 | GOTRUE_SMS_PROVIDER: 'twilio' 36 | GOTRUE_SMS_TWILIO_ACCOUNT_SID: '${GOTRUE_SMS_TWILIO_ACCOUNT_SID}' 37 | GOTRUE_SMS_TWILIO_AUTH_TOKEN: '${GOTRUE_SMS_TWILIO_AUTH_TOKEN}' 38 | GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: '${GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID}' 39 | GOTRUE_SMS_AUTOCONFIRM: 'false' 40 | GOTRUE_COOKIE_KEY: 'sb' 41 | depends_on: 42 | - db 43 | restart: on-failure 44 | autoconfirm: # Signup enabled, autoconfirm on 45 | image: supabase/auth:v2.151.0 46 | ports: 47 | - '9998:9998' 48 | environment: 49 | GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' 50 | GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["sign", "verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["verify"],"alg":"RS256"}]' 51 | GOTRUE_JWT_EXP: 3600 52 | GOTRUE_DB_DRIVER: postgres 53 | DB_NAMESPACE: auth 54 | GOTRUE_API_HOST: 0.0.0.0 55 | PORT: 9998 56 | GOTRUE_DISABLE_SIGNUP: 'false' 57 | API_EXTERNAL_URL: http://localhost:9998 58 | GOTRUE_SITE_URL: http://localhost:9998 59 | GOTRUE_MAILER_AUTOCONFIRM: 'true' 60 | GOTRUE_SMS_AUTOCONFIRM: 'true' 61 | GOTRUE_LOG_LEVEL: DEBUG 62 | GOTRUE_OPERATOR_TOKEN: super-secret-operator-token 63 | DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' 64 | GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' 65 | GOTRUE_SMTP_HOST: mail 66 | GOTRUE_SMTP_PORT: 2500 67 | GOTRUE_SMTP_USER: GOTRUE_SMTP_USER 68 | GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS 69 | GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com 70 | GOTRUE_COOKIE_KEY: 'sb' 71 | GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true' 72 | depends_on: 73 | - db 74 | restart: on-failure 75 | autoconfirm_with_asymmetric_keys: # Signup enabled, autoconfirm on 76 | image: supabase/auth:v2.169.0 77 | ports: 78 | - '9996:9996' 79 | environment: 80 | GOTRUE_JWT_SECRET: 'Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo' 81 | GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["sign","verify"],"alg":"RS256"}]' 82 | GOTRUE_JWT_EXP: 3600 83 | GOTRUE_DB_DRIVER: postgres 84 | DB_NAMESPACE: auth 85 | GOTRUE_API_HOST: 0.0.0.0 86 | PORT: 9996 87 | GOTRUE_DISABLE_SIGNUP: 'false' 88 | API_EXTERNAL_URL: http://localhost:9996 89 | GOTRUE_SITE_URL: http://localhost:9996 90 | GOTRUE_MAILER_AUTOCONFIRM: 'true' 91 | GOTRUE_SMS_AUTOCONFIRM: 'true' 92 | GOTRUE_LOG_LEVEL: DEBUG 93 | GOTRUE_OPERATOR_TOKEN: super-secret-operator-token 94 | DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' 95 | GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' 96 | GOTRUE_SMTP_HOST: mail 97 | GOTRUE_SMTP_PORT: 2500 98 | GOTRUE_SMTP_USER: GOTRUE_SMTP_USER 99 | GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS 100 | GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com 101 | GOTRUE_COOKIE_KEY: 'sb' 102 | depends_on: 103 | - db 104 | restart: on-failure 105 | disabled: # Signup disabled 106 | image: supabase/auth:v2.151.0 107 | ports: 108 | - '9997:9997' 109 | environment: 110 | GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' 111 | GOTRUE_JWT_EXP: 3600 112 | GOTRUE_DB_DRIVER: postgres 113 | DB_NAMESPACE: auth 114 | GOTRUE_API_HOST: 0.0.0.0 115 | PORT: 9997 116 | GOTRUE_DISABLE_SIGNUP: 'true' 117 | API_EXTERNAL_URL: http://localhost:9997 118 | GOTRUE_SITE_URL: http://localhost:9997 119 | GOTRUE_MAILER_AUTOCONFIRM: 'false' 120 | GOTRUE_LOG_LEVEL: DEBUG 121 | GOTRUE_OPERATOR_TOKEN: super-secret-operator-token 122 | DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' 123 | GOTRUE_EXTERNAL_PHONE_ENABLED: 'false' 124 | GOTRUE_EXTERNAL_EMAIL_ENABLED: 'false' 125 | GOTRUE_SMTP_HOST: mail 126 | GOTRUE_SMTP_PORT: 2500 127 | GOTRUE_SMTP_USER: GOTRUE_SMTP_USER 128 | GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS 129 | GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com 130 | GOTRUE_COOKIE_KEY: 'sb' 131 | depends_on: 132 | - db 133 | restart: on-failure 134 | mail: 135 | image: phamhieu/inbucket:latest 136 | ports: 137 | - '2500:2500' # SMTP 138 | - '9000:9000' # web interface 139 | - '1100:1100' # POP3 140 | db: 141 | image: supabase/postgres:15.1.1.46 142 | ports: 143 | - '5432:5432' 144 | command: postgres -c config_file=/etc/postgresql/postgresql.conf 145 | volumes: 146 | - ./db:/docker-entrypoint-initdb.d/ 147 | environment: 148 | POSTGRES_DB: postgres 149 | POSTGRES_USER: postgres 150 | POSTGRES_PASSWORD: postgres 151 | POSTGRES_PORT: 5432 152 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | coverageDirectory: './test/coverage', 6 | coverageReporters: ['json', 'html', 'lcov'], 7 | collectCoverageFrom: [ 8 | './src/**/*.{js,ts}', 9 | './src/**/*.unit.test.ts', 10 | '!**/node_modules/**', 11 | '!**/vendor/**', 12 | '!**/vendor/**', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase/auth-js", 3 | "version": "0.0.0", 4 | "private": false, 5 | "description": "Official client library for Supabase Auth", 6 | "keywords": [ 7 | "auth", 8 | "supabase", 9 | "auth", 10 | "authentication" 11 | ], 12 | "homepage": "https://github.com/supabase/auth-js", 13 | "bugs": "https://github.com/supabase/auth-js/issues", 14 | "license": "MIT", 15 | "author": "Supabase", 16 | "files": [ 17 | "dist", 18 | "src" 19 | ], 20 | "main": "dist/main/index.js", 21 | "module": "dist/module/index.js", 22 | "types": "dist/module/index.d.ts", 23 | "repository": "github:supabase/auth-js", 24 | "scripts": { 25 | "clean": "rimraf dist docs", 26 | "coverage": "echo \"run npm test\"", 27 | "format": "prettier --write \"{src,test}/**/*.ts\"", 28 | "build": "genversion src/lib/version.ts --es6 && run-s clean format build:* && run-s lint", 29 | "build:main": "tsc -p tsconfig.json", 30 | "build:module": "tsc -p tsconfig.module.json", 31 | "lint": "eslint ./src/**/* test/**/*.test.ts", 32 | "test": "run-s test:clean test:infra test:suite test:clean", 33 | "test:suite": "jest --runInBand --coverage", 34 | "test:infra": "cd infra && docker compose down && docker compose pull && docker compose up -d && sleep 30", 35 | "test:clean": "cd infra && docker compose down", 36 | "docs": "typedoc src/index.ts --out docs/v2 --excludePrivate --excludeProtected", 37 | "docs:json": "typedoc --json docs/v2/spec.json --excludeExternals --excludePrivate --excludeProtected src/index.ts" 38 | }, 39 | "dependencies": { 40 | "@supabase/node-fetch": "^2.6.14" 41 | }, 42 | "devDependencies": { 43 | "@solana/wallet-standard-features": "^1.3.0", 44 | "@types/faker": "^5.1.6", 45 | "@types/jest": "^28.1.6", 46 | "@types/jsonwebtoken": "^8.5.6", 47 | "@types/node": "^18.16.19", 48 | "@types/node-fetch": "^2.6.4", 49 | "@typescript-eslint/eslint-plugin": "^5.30.7", 50 | "@typescript-eslint/parser": "^5.30.7", 51 | "eslint": "^8.20.0", 52 | "eslint-config-prettier": "^8.5.0", 53 | "eslint-config-standard": "^17.0.0", 54 | "eslint-plugin-import": "^2.26.0", 55 | "eslint-plugin-node": "^11.1.0", 56 | "eslint-plugin-promise": "^6.0.0", 57 | "faker": "^5.3.1", 58 | "genversion": "^3.1.1", 59 | "jest": "^28.1.3", 60 | "jest-mock-server": "^0.1.0", 61 | "jsonwebtoken": "^9.0.0", 62 | "npm-run-all": "^4.1.5", 63 | "prettier": "2.7.1", 64 | "rimraf": "^3.0.2", 65 | "semantic-release-plugin-update-version-in-files": "^1.1.0", 66 | "ts-jest": "^28.0.7", 67 | "typedoc": "^0.22.16", 68 | "typescript": "^4.7.4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AuthAdminApi.ts: -------------------------------------------------------------------------------- 1 | import GoTrueAdminApi from './GoTrueAdminApi' 2 | 3 | const AuthAdminApi = GoTrueAdminApi 4 | 5 | export default AuthAdminApi 6 | -------------------------------------------------------------------------------- /src/AuthClient.ts: -------------------------------------------------------------------------------- 1 | import GoTrueClient from './GoTrueClient' 2 | 3 | const AuthClient = GoTrueClient 4 | 5 | export default AuthClient 6 | -------------------------------------------------------------------------------- /src/GoTrueAdminApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Fetch, 3 | _generateLinkResponse, 4 | _noResolveJsonResponse, 5 | _request, 6 | _userResponse, 7 | } from './lib/fetch' 8 | import { resolveFetch, validateUUID } from './lib/helpers' 9 | import { 10 | AdminUserAttributes, 11 | GenerateLinkParams, 12 | GenerateLinkResponse, 13 | Pagination, 14 | User, 15 | UserResponse, 16 | GoTrueAdminMFAApi, 17 | AuthMFAAdminDeleteFactorParams, 18 | AuthMFAAdminDeleteFactorResponse, 19 | AuthMFAAdminListFactorsParams, 20 | AuthMFAAdminListFactorsResponse, 21 | PageParams, 22 | SIGN_OUT_SCOPES, 23 | SignOutScope, 24 | } from './lib/types' 25 | import { AuthError, isAuthError } from './lib/errors' 26 | 27 | export default class GoTrueAdminApi { 28 | /** Contains all MFA administration methods. */ 29 | mfa: GoTrueAdminMFAApi 30 | 31 | protected url: string 32 | protected headers: { 33 | [key: string]: string 34 | } 35 | protected fetch: Fetch 36 | 37 | constructor({ 38 | url = '', 39 | headers = {}, 40 | fetch, 41 | }: { 42 | url: string 43 | headers?: { 44 | [key: string]: string 45 | } 46 | fetch?: Fetch 47 | }) { 48 | this.url = url 49 | this.headers = headers 50 | this.fetch = resolveFetch(fetch) 51 | this.mfa = { 52 | listFactors: this._listFactors.bind(this), 53 | deleteFactor: this._deleteFactor.bind(this), 54 | } 55 | } 56 | 57 | /** 58 | * Removes a logged-in session. 59 | * @param jwt A valid, logged-in JWT. 60 | * @param scope The logout sope. 61 | */ 62 | async signOut( 63 | jwt: string, 64 | scope: SignOutScope = SIGN_OUT_SCOPES[0] 65 | ): Promise<{ data: null; error: AuthError | null }> { 66 | if (SIGN_OUT_SCOPES.indexOf(scope) < 0) { 67 | throw new Error( 68 | `@supabase/auth-js: Parameter scope must be one of ${SIGN_OUT_SCOPES.join(', ')}` 69 | ) 70 | } 71 | 72 | try { 73 | await _request(this.fetch, 'POST', `${this.url}/logout?scope=${scope}`, { 74 | headers: this.headers, 75 | jwt, 76 | noResolveJson: true, 77 | }) 78 | return { data: null, error: null } 79 | } catch (error) { 80 | if (isAuthError(error)) { 81 | return { data: null, error } 82 | } 83 | 84 | throw error 85 | } 86 | } 87 | 88 | /** 89 | * Sends an invite link to an email address. 90 | * @param email The email address of the user. 91 | * @param options Additional options to be included when inviting. 92 | */ 93 | async inviteUserByEmail( 94 | email: string, 95 | options: { 96 | /** A custom data object to store additional metadata about the user. This maps to the `auth.users.user_metadata` column. */ 97 | data?: object 98 | 99 | /** The URL which will be appended to the email link sent to the user's email address. Once clicked the user will end up on this URL. */ 100 | redirectTo?: string 101 | } = {} 102 | ): Promise { 103 | try { 104 | return await _request(this.fetch, 'POST', `${this.url}/invite`, { 105 | body: { email, data: options.data }, 106 | headers: this.headers, 107 | redirectTo: options.redirectTo, 108 | xform: _userResponse, 109 | }) 110 | } catch (error) { 111 | if (isAuthError(error)) { 112 | return { data: { user: null }, error } 113 | } 114 | 115 | throw error 116 | } 117 | } 118 | 119 | /** 120 | * Generates email links and OTPs to be sent via a custom email provider. 121 | * @param email The user's email. 122 | * @param options.password User password. For signup only. 123 | * @param options.data Optional user metadata. For signup only. 124 | * @param options.redirectTo The redirect url which should be appended to the generated link 125 | */ 126 | async generateLink(params: GenerateLinkParams): Promise { 127 | try { 128 | const { options, ...rest } = params 129 | const body: any = { ...rest, ...options } 130 | if ('newEmail' in rest) { 131 | // replace newEmail with new_email in request body 132 | body.new_email = rest?.newEmail 133 | delete body['newEmail'] 134 | } 135 | return await _request(this.fetch, 'POST', `${this.url}/admin/generate_link`, { 136 | body: body, 137 | headers: this.headers, 138 | xform: _generateLinkResponse, 139 | redirectTo: options?.redirectTo, 140 | }) 141 | } catch (error) { 142 | if (isAuthError(error)) { 143 | return { 144 | data: { 145 | properties: null, 146 | user: null, 147 | }, 148 | error, 149 | } 150 | } 151 | throw error 152 | } 153 | } 154 | 155 | // User Admin API 156 | /** 157 | * Creates a new user. 158 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 159 | */ 160 | async createUser(attributes: AdminUserAttributes): Promise { 161 | try { 162 | return await _request(this.fetch, 'POST', `${this.url}/admin/users`, { 163 | body: attributes, 164 | headers: this.headers, 165 | xform: _userResponse, 166 | }) 167 | } catch (error) { 168 | if (isAuthError(error)) { 169 | return { data: { user: null }, error } 170 | } 171 | 172 | throw error 173 | } 174 | } 175 | 176 | /** 177 | * Get a list of users. 178 | * 179 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 180 | * @param params An object which supports `page` and `perPage` as numbers, to alter the paginated results. 181 | */ 182 | async listUsers( 183 | params?: PageParams 184 | ): Promise< 185 | | { data: { users: User[]; aud: string } & Pagination; error: null } 186 | | { data: { users: [] }; error: AuthError } 187 | > { 188 | try { 189 | const pagination: Pagination = { nextPage: null, lastPage: 0, total: 0 } 190 | const response = await _request(this.fetch, 'GET', `${this.url}/admin/users`, { 191 | headers: this.headers, 192 | noResolveJson: true, 193 | query: { 194 | page: params?.page?.toString() ?? '', 195 | per_page: params?.perPage?.toString() ?? '', 196 | }, 197 | xform: _noResolveJsonResponse, 198 | }) 199 | if (response.error) throw response.error 200 | 201 | const users = await response.json() 202 | const total = response.headers.get('x-total-count') ?? 0 203 | const links = response.headers.get('link')?.split(',') ?? [] 204 | if (links.length > 0) { 205 | links.forEach((link: string) => { 206 | const page = parseInt(link.split(';')[0].split('=')[1].substring(0, 1)) 207 | const rel = JSON.parse(link.split(';')[1].split('=')[1]) 208 | pagination[`${rel}Page`] = page 209 | }) 210 | 211 | pagination.total = parseInt(total) 212 | } 213 | return { data: { ...users, ...pagination }, error: null } 214 | } catch (error) { 215 | if (isAuthError(error)) { 216 | return { data: { users: [] }, error } 217 | } 218 | throw error 219 | } 220 | } 221 | 222 | /** 223 | * Get user by id. 224 | * 225 | * @param uid The user's unique identifier 226 | * 227 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 228 | */ 229 | async getUserById(uid: string): Promise { 230 | validateUUID(uid) 231 | 232 | try { 233 | return await _request(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, { 234 | headers: this.headers, 235 | xform: _userResponse, 236 | }) 237 | } catch (error) { 238 | if (isAuthError(error)) { 239 | return { data: { user: null }, error } 240 | } 241 | 242 | throw error 243 | } 244 | } 245 | 246 | /** 247 | * Updates the user data. 248 | * 249 | * @param attributes The data you want to update. 250 | * 251 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 252 | */ 253 | async updateUserById(uid: string, attributes: AdminUserAttributes): Promise { 254 | validateUUID(uid) 255 | 256 | try { 257 | return await _request(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, { 258 | body: attributes, 259 | headers: this.headers, 260 | xform: _userResponse, 261 | }) 262 | } catch (error) { 263 | if (isAuthError(error)) { 264 | return { data: { user: null }, error } 265 | } 266 | 267 | throw error 268 | } 269 | } 270 | 271 | /** 272 | * Delete a user. Requires a `service_role` key. 273 | * 274 | * @param id The user id you want to remove. 275 | * @param shouldSoftDelete If true, then the user will be soft-deleted from the auth schema. Soft deletion allows user identification from the hashed user ID but is not reversible. 276 | * Defaults to false for backward compatibility. 277 | * 278 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 279 | */ 280 | async deleteUser(id: string, shouldSoftDelete = false): Promise { 281 | validateUUID(id) 282 | 283 | try { 284 | return await _request(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, { 285 | headers: this.headers, 286 | body: { 287 | should_soft_delete: shouldSoftDelete, 288 | }, 289 | xform: _userResponse, 290 | }) 291 | } catch (error) { 292 | if (isAuthError(error)) { 293 | return { data: { user: null }, error } 294 | } 295 | 296 | throw error 297 | } 298 | } 299 | 300 | private async _listFactors( 301 | params: AuthMFAAdminListFactorsParams 302 | ): Promise { 303 | validateUUID(params.userId) 304 | 305 | try { 306 | const { data, error } = await _request( 307 | this.fetch, 308 | 'GET', 309 | `${this.url}/admin/users/${params.userId}/factors`, 310 | { 311 | headers: this.headers, 312 | xform: (factors: any) => { 313 | return { data: { factors }, error: null } 314 | }, 315 | } 316 | ) 317 | return { data, error } 318 | } catch (error) { 319 | if (isAuthError(error)) { 320 | return { data: null, error } 321 | } 322 | 323 | throw error 324 | } 325 | } 326 | 327 | private async _deleteFactor( 328 | params: AuthMFAAdminDeleteFactorParams 329 | ): Promise { 330 | validateUUID(params.userId) 331 | validateUUID(params.id) 332 | 333 | try { 334 | const data = await _request( 335 | this.fetch, 336 | 'DELETE', 337 | `${this.url}/admin/users/${params.userId}/factors/${params.id}`, 338 | { 339 | headers: this.headers, 340 | } 341 | ) 342 | 343 | return { data, error: null } 344 | } catch (error) { 345 | if (isAuthError(error)) { 346 | return { data: null, error } 347 | } 348 | 349 | throw error 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import GoTrueAdminApi from './GoTrueAdminApi' 2 | import GoTrueClient from './GoTrueClient' 3 | import AuthAdminApi from './AuthAdminApi' 4 | import AuthClient from './AuthClient' 5 | export { GoTrueAdminApi, GoTrueClient, AuthAdminApi, AuthClient } 6 | export * from './lib/types' 7 | export * from './lib/errors' 8 | export { 9 | navigatorLock, 10 | NavigatorLockAcquireTimeoutError, 11 | internals as lockInternals, 12 | processLock, 13 | } from './lib/locks' 14 | -------------------------------------------------------------------------------- /src/lib/base64url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Avoid modifying this file. It's part of 3 | * https://github.com/supabase-community/base64url-js. Submit all fixes on 4 | * that repo! 5 | */ 6 | 7 | /** 8 | * An array of characters that encode 6 bits into a Base64-URL alphabet 9 | * character. 10 | */ 11 | const TO_BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('') 12 | 13 | /** 14 | * An array of characters that can appear in a Base64-URL encoded string but 15 | * should be ignored. 16 | */ 17 | const IGNORE_BASE64URL = ' \t\n\r='.split('') 18 | 19 | /** 20 | * An array of 128 numbers that map a Base64-URL character to 6 bits, or if -2 21 | * used to skip the character, or if -1 used to error out. 22 | */ 23 | const FROM_BASE64URL = (() => { 24 | const charMap: number[] = new Array(128) 25 | 26 | for (let i = 0; i < charMap.length; i += 1) { 27 | charMap[i] = -1 28 | } 29 | 30 | for (let i = 0; i < IGNORE_BASE64URL.length; i += 1) { 31 | charMap[IGNORE_BASE64URL[i].charCodeAt(0)] = -2 32 | } 33 | 34 | for (let i = 0; i < TO_BASE64URL.length; i += 1) { 35 | charMap[TO_BASE64URL[i].charCodeAt(0)] = i 36 | } 37 | 38 | return charMap 39 | })() 40 | 41 | /** 42 | * Converts a byte to a Base64-URL string. 43 | * 44 | * @param byte The byte to convert, or null to flush at the end of the byte sequence. 45 | * @param state The Base64 conversion state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`. 46 | * @param emit A function called with the next Base64 character when ready. 47 | */ 48 | export function byteToBase64URL( 49 | byte: number | null, 50 | state: { queue: number; queuedBits: number }, 51 | emit: (char: string) => void 52 | ) { 53 | if (byte !== null) { 54 | state.queue = (state.queue << 8) | byte 55 | state.queuedBits += 8 56 | 57 | while (state.queuedBits >= 6) { 58 | const pos = (state.queue >> (state.queuedBits - 6)) & 63 59 | emit(TO_BASE64URL[pos]) 60 | state.queuedBits -= 6 61 | } 62 | } else if (state.queuedBits > 0) { 63 | state.queue = state.queue << (6 - state.queuedBits) 64 | state.queuedBits = 6 65 | 66 | while (state.queuedBits >= 6) { 67 | const pos = (state.queue >> (state.queuedBits - 6)) & 63 68 | emit(TO_BASE64URL[pos]) 69 | state.queuedBits -= 6 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Converts a String char code (extracted using `string.charCodeAt(position)`) to a sequence of Base64-URL characters. 76 | * 77 | * @param charCode The char code of the JavaScript string. 78 | * @param state The Base64 state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`. 79 | * @param emit A function called with the next byte. 80 | */ 81 | export function byteFromBase64URL( 82 | charCode: number, 83 | state: { queue: number; queuedBits: number }, 84 | emit: (byte: number) => void 85 | ) { 86 | const bits = FROM_BASE64URL[charCode] 87 | 88 | if (bits > -1) { 89 | // valid Base64-URL character 90 | state.queue = (state.queue << 6) | bits 91 | state.queuedBits += 6 92 | 93 | while (state.queuedBits >= 8) { 94 | emit((state.queue >> (state.queuedBits - 8)) & 0xff) 95 | state.queuedBits -= 8 96 | } 97 | } else if (bits === -2) { 98 | // ignore spaces, tabs, newlines, = 99 | return 100 | } else { 101 | throw new Error(`Invalid Base64-URL character "${String.fromCharCode(charCode)}"`) 102 | } 103 | } 104 | 105 | /** 106 | * Converts a JavaScript string (which may include any valid character) into a 107 | * Base64-URL encoded string. The string is first encoded in UTF-8 which is 108 | * then encoded as Base64-URL. 109 | * 110 | * @param str The string to convert. 111 | */ 112 | export function stringToBase64URL(str: string) { 113 | const base64: string[] = [] 114 | 115 | const emitter = (char: string) => { 116 | base64.push(char) 117 | } 118 | 119 | const state = { queue: 0, queuedBits: 0 } 120 | 121 | stringToUTF8(str, (byte: number) => { 122 | byteToBase64URL(byte, state, emitter) 123 | }) 124 | 125 | byteToBase64URL(null, state, emitter) 126 | 127 | return base64.join('') 128 | } 129 | 130 | /** 131 | * Converts a Base64-URL encoded string into a JavaScript string. It is assumed 132 | * that the underlying string has been encoded as UTF-8. 133 | * 134 | * @param str The Base64-URL encoded string. 135 | */ 136 | export function stringFromBase64URL(str: string) { 137 | const conv: string[] = [] 138 | 139 | const utf8Emit = (codepoint: number) => { 140 | conv.push(String.fromCodePoint(codepoint)) 141 | } 142 | 143 | const utf8State = { 144 | utf8seq: 0, 145 | codepoint: 0, 146 | } 147 | 148 | const b64State = { queue: 0, queuedBits: 0 } 149 | 150 | const byteEmit = (byte: number) => { 151 | stringFromUTF8(byte, utf8State, utf8Emit) 152 | } 153 | 154 | for (let i = 0; i < str.length; i += 1) { 155 | byteFromBase64URL(str.charCodeAt(i), b64State, byteEmit) 156 | } 157 | 158 | return conv.join('') 159 | } 160 | 161 | /** 162 | * Converts a Unicode codepoint to a multi-byte UTF-8 sequence. 163 | * 164 | * @param codepoint The Unicode codepoint. 165 | * @param emit Function which will be called for each UTF-8 byte that represents the codepoint. 166 | */ 167 | export function codepointToUTF8(codepoint: number, emit: (byte: number) => void) { 168 | if (codepoint <= 0x7f) { 169 | emit(codepoint) 170 | return 171 | } else if (codepoint <= 0x7ff) { 172 | emit(0xc0 | (codepoint >> 6)) 173 | emit(0x80 | (codepoint & 0x3f)) 174 | return 175 | } else if (codepoint <= 0xffff) { 176 | emit(0xe0 | (codepoint >> 12)) 177 | emit(0x80 | ((codepoint >> 6) & 0x3f)) 178 | emit(0x80 | (codepoint & 0x3f)) 179 | return 180 | } else if (codepoint <= 0x10ffff) { 181 | emit(0xf0 | (codepoint >> 18)) 182 | emit(0x80 | ((codepoint >> 12) & 0x3f)) 183 | emit(0x80 | ((codepoint >> 6) & 0x3f)) 184 | emit(0x80 | (codepoint & 0x3f)) 185 | return 186 | } 187 | 188 | throw new Error(`Unrecognized Unicode codepoint: ${codepoint.toString(16)}`) 189 | } 190 | 191 | /** 192 | * Converts a JavaScript string to a sequence of UTF-8 bytes. 193 | * 194 | * @param str The string to convert to UTF-8. 195 | * @param emit Function which will be called for each UTF-8 byte of the string. 196 | */ 197 | export function stringToUTF8(str: string, emit: (byte: number) => void) { 198 | for (let i = 0; i < str.length; i += 1) { 199 | let codepoint = str.charCodeAt(i) 200 | 201 | if (codepoint > 0xd7ff && codepoint <= 0xdbff) { 202 | // most UTF-16 codepoints are Unicode codepoints, except values in this 203 | // range where the next UTF-16 codepoint needs to be combined with the 204 | // current one to get the Unicode codepoint 205 | const highSurrogate = ((codepoint - 0xd800) * 0x400) & 0xffff 206 | const lowSurrogate = (str.charCodeAt(i + 1) - 0xdc00) & 0xffff 207 | codepoint = (lowSurrogate | highSurrogate) + 0x10000 208 | i += 1 209 | } 210 | 211 | codepointToUTF8(codepoint, emit) 212 | } 213 | } 214 | 215 | /** 216 | * Converts a UTF-8 byte to a Unicode codepoint. 217 | * 218 | * @param byte The UTF-8 byte next in the sequence. 219 | * @param state The shared state between consecutive UTF-8 bytes in the 220 | * sequence, an object with the shape `{ utf8seq: 0, codepoint: 0 }`. 221 | * @param emit Function which will be called for each codepoint. 222 | */ 223 | export function stringFromUTF8( 224 | byte: number, 225 | state: { utf8seq: number; codepoint: number }, 226 | emit: (codepoint: number) => void 227 | ) { 228 | if (state.utf8seq === 0) { 229 | if (byte <= 0x7f) { 230 | emit(byte) 231 | return 232 | } 233 | 234 | // count the number of 1 leading bits until you reach 0 235 | for (let leadingBit = 1; leadingBit < 6; leadingBit += 1) { 236 | if (((byte >> (7 - leadingBit)) & 1) === 0) { 237 | state.utf8seq = leadingBit 238 | break 239 | } 240 | } 241 | 242 | if (state.utf8seq === 2) { 243 | state.codepoint = byte & 31 244 | } else if (state.utf8seq === 3) { 245 | state.codepoint = byte & 15 246 | } else if (state.utf8seq === 4) { 247 | state.codepoint = byte & 7 248 | } else { 249 | throw new Error('Invalid UTF-8 sequence') 250 | } 251 | 252 | state.utf8seq -= 1 253 | } else if (state.utf8seq > 0) { 254 | if (byte <= 0x7f) { 255 | throw new Error('Invalid UTF-8 sequence') 256 | } 257 | 258 | state.codepoint = (state.codepoint << 6) | (byte & 63) 259 | state.utf8seq -= 1 260 | 261 | if (state.utf8seq === 0) { 262 | emit(state.codepoint) 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Helper functions to convert different types of strings to Uint8Array 269 | */ 270 | 271 | export function base64UrlToUint8Array(str: string): Uint8Array { 272 | const result: number[] = [] 273 | const state = { queue: 0, queuedBits: 0 } 274 | 275 | const onByte = (byte: number) => { 276 | result.push(byte) 277 | } 278 | 279 | for (let i = 0; i < str.length; i += 1) { 280 | byteFromBase64URL(str.charCodeAt(i), state, onByte) 281 | } 282 | 283 | return new Uint8Array(result) 284 | } 285 | 286 | export function stringToUint8Array(str: string): Uint8Array { 287 | const result: number[] = [] 288 | stringToUTF8(str, (byte: number) => result.push(byte)) 289 | return new Uint8Array(result) 290 | } 291 | 292 | export function bytesToBase64URL(bytes: Uint8Array) { 293 | const result: string[] = [] 294 | const state = { queue: 0, queuedBits: 0 } 295 | 296 | const onChar = (char: string) => { 297 | result.push(char) 298 | } 299 | 300 | bytes.forEach((byte) => byteToBase64URL(byte, state, onChar)) 301 | 302 | // always call with `null` after processing all bytes 303 | byteToBase64URL(null, state, onChar) 304 | 305 | return result.join('') 306 | } 307 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version' 2 | 3 | /** Current session will be checked for refresh at this interval. */ 4 | export const AUTO_REFRESH_TICK_DURATION_MS = 30 * 1000 5 | 6 | /** 7 | * A token refresh will be attempted this many ticks before the current session expires. */ 8 | export const AUTO_REFRESH_TICK_THRESHOLD = 3 9 | 10 | /* 11 | * Earliest time before an access token expires that the session should be refreshed. 12 | */ 13 | export const EXPIRY_MARGIN_MS = AUTO_REFRESH_TICK_THRESHOLD * AUTO_REFRESH_TICK_DURATION_MS 14 | 15 | export const GOTRUE_URL = 'http://localhost:9999' 16 | export const STORAGE_KEY = 'supabase.auth.token' 17 | export const AUDIENCE = '' 18 | export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } 19 | export const NETWORK_FAILURE = { 20 | MAX_RETRIES: 10, 21 | RETRY_INTERVAL: 2, // in deciseconds 22 | } 23 | 24 | export const API_VERSION_HEADER_NAME = 'X-Supabase-Api-Version' 25 | export const API_VERSIONS = { 26 | '2024-01-01': { 27 | timestamp: Date.parse('2024-01-01T00:00:00.0Z'), 28 | name: '2024-01-01', 29 | }, 30 | } 31 | 32 | export const BASE64URL_REGEX = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i 33 | 34 | export const JWKS_TTL = 600000 // 10 minutes 35 | -------------------------------------------------------------------------------- /src/lib/error-codes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Known error codes. Note that the server may also return other error codes 3 | * not included in this list (if the client library is older than the version 4 | * on the server). 5 | */ 6 | export type ErrorCode = 7 | | 'unexpected_failure' 8 | | 'validation_failed' 9 | | 'bad_json' 10 | | 'email_exists' 11 | | 'phone_exists' 12 | | 'bad_jwt' 13 | | 'not_admin' 14 | | 'no_authorization' 15 | | 'user_not_found' 16 | | 'session_not_found' 17 | | 'session_expired' 18 | | 'refresh_token_not_found' 19 | | 'refresh_token_already_used' 20 | | 'flow_state_not_found' 21 | | 'flow_state_expired' 22 | | 'signup_disabled' 23 | | 'user_banned' 24 | | 'provider_email_needs_verification' 25 | | 'invite_not_found' 26 | | 'bad_oauth_state' 27 | | 'bad_oauth_callback' 28 | | 'oauth_provider_not_supported' 29 | | 'unexpected_audience' 30 | | 'single_identity_not_deletable' 31 | | 'email_conflict_identity_not_deletable' 32 | | 'identity_already_exists' 33 | | 'email_provider_disabled' 34 | | 'phone_provider_disabled' 35 | | 'too_many_enrolled_mfa_factors' 36 | | 'mfa_factor_name_conflict' 37 | | 'mfa_factor_not_found' 38 | | 'mfa_ip_address_mismatch' 39 | | 'mfa_challenge_expired' 40 | | 'mfa_verification_failed' 41 | | 'mfa_verification_rejected' 42 | | 'insufficient_aal' 43 | | 'captcha_failed' 44 | | 'saml_provider_disabled' 45 | | 'manual_linking_disabled' 46 | | 'sms_send_failed' 47 | | 'email_not_confirmed' 48 | | 'phone_not_confirmed' 49 | | 'reauth_nonce_missing' 50 | | 'saml_relay_state_not_found' 51 | | 'saml_relay_state_expired' 52 | | 'saml_idp_not_found' 53 | | 'saml_assertion_no_user_id' 54 | | 'saml_assertion_no_email' 55 | | 'user_already_exists' 56 | | 'sso_provider_not_found' 57 | | 'saml_metadata_fetch_failed' 58 | | 'saml_idp_already_exists' 59 | | 'sso_domain_already_exists' 60 | | 'saml_entity_id_mismatch' 61 | | 'conflict' 62 | | 'provider_disabled' 63 | | 'user_sso_managed' 64 | | 'reauthentication_needed' 65 | | 'same_password' 66 | | 'reauthentication_not_valid' 67 | | 'otp_expired' 68 | | 'otp_disabled' 69 | | 'identity_not_found' 70 | | 'weak_password' 71 | | 'over_request_rate_limit' 72 | | 'over_email_send_rate_limit' 73 | | 'over_sms_send_rate_limit' 74 | | 'bad_code_verifier' 75 | | 'anonymous_provider_disabled' 76 | | 'hook_timeout' 77 | | 'hook_timeout_after_retry' 78 | | 'hook_payload_over_size_limit' 79 | | 'hook_payload_invalid_content_type' 80 | | 'request_timeout' 81 | | 'mfa_phone_enroll_not_enabled' 82 | | 'mfa_phone_verify_not_enabled' 83 | | 'mfa_totp_enroll_not_enabled' 84 | | 'mfa_totp_verify_not_enabled' 85 | | 'mfa_webauthn_enroll_not_enabled' 86 | | 'mfa_webauthn_verify_not_enabled' 87 | | 'mfa_verified_factor_exists' 88 | | 'invalid_credentials' 89 | | 'email_address_not_authorized' 90 | | 'email_address_invalid' 91 | -------------------------------------------------------------------------------- /src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | import { WeakPasswordReasons } from './types' 2 | import { ErrorCode } from './error-codes' 3 | 4 | export class AuthError extends Error { 5 | /** 6 | * Error code associated with the error. Most errors coming from 7 | * HTTP responses will have a code, though some errors that occur 8 | * before a response is received will not have one present. In that 9 | * case {@link #status} will also be undefined. 10 | */ 11 | code: ErrorCode | (string & {}) | undefined 12 | 13 | /** HTTP status code that caused the error. */ 14 | status: number | undefined 15 | 16 | protected __isAuthError = true 17 | 18 | constructor(message: string, status?: number, code?: string) { 19 | super(message) 20 | this.name = 'AuthError' 21 | this.status = status 22 | this.code = code 23 | } 24 | } 25 | 26 | export function isAuthError(error: unknown): error is AuthError { 27 | return typeof error === 'object' && error !== null && '__isAuthError' in error 28 | } 29 | 30 | export class AuthApiError extends AuthError { 31 | status: number 32 | 33 | constructor(message: string, status: number, code: string | undefined) { 34 | super(message, status, code) 35 | this.name = 'AuthApiError' 36 | this.status = status 37 | this.code = code 38 | } 39 | } 40 | 41 | export function isAuthApiError(error: unknown): error is AuthApiError { 42 | return isAuthError(error) && error.name === 'AuthApiError' 43 | } 44 | 45 | export class AuthUnknownError extends AuthError { 46 | originalError: unknown 47 | 48 | constructor(message: string, originalError: unknown) { 49 | super(message) 50 | this.name = 'AuthUnknownError' 51 | this.originalError = originalError 52 | } 53 | } 54 | 55 | export class CustomAuthError extends AuthError { 56 | name: string 57 | status: number 58 | 59 | constructor(message: string, name: string, status: number, code: string | undefined) { 60 | super(message, status, code) 61 | this.name = name 62 | this.status = status 63 | } 64 | } 65 | 66 | export class AuthSessionMissingError extends CustomAuthError { 67 | constructor() { 68 | super('Auth session missing!', 'AuthSessionMissingError', 400, undefined) 69 | } 70 | } 71 | 72 | export function isAuthSessionMissingError(error: any): error is AuthSessionMissingError { 73 | return isAuthError(error) && error.name === 'AuthSessionMissingError' 74 | } 75 | 76 | export class AuthInvalidTokenResponseError extends CustomAuthError { 77 | constructor() { 78 | super('Auth session or user missing', 'AuthInvalidTokenResponseError', 500, undefined) 79 | } 80 | } 81 | 82 | export class AuthInvalidCredentialsError extends CustomAuthError { 83 | constructor(message: string) { 84 | super(message, 'AuthInvalidCredentialsError', 400, undefined) 85 | } 86 | } 87 | 88 | export class AuthImplicitGrantRedirectError extends CustomAuthError { 89 | details: { error: string; code: string } | null = null 90 | constructor(message: string, details: { error: string; code: string } | null = null) { 91 | super(message, 'AuthImplicitGrantRedirectError', 500, undefined) 92 | this.details = details 93 | } 94 | 95 | toJSON() { 96 | return { 97 | name: this.name, 98 | message: this.message, 99 | status: this.status, 100 | details: this.details, 101 | } 102 | } 103 | } 104 | 105 | export function isAuthImplicitGrantRedirectError( 106 | error: any 107 | ): error is AuthImplicitGrantRedirectError { 108 | return isAuthError(error) && error.name === 'AuthImplicitGrantRedirectError' 109 | } 110 | 111 | export class AuthPKCEGrantCodeExchangeError extends CustomAuthError { 112 | details: { error: string; code: string } | null = null 113 | 114 | constructor(message: string, details: { error: string; code: string } | null = null) { 115 | super(message, 'AuthPKCEGrantCodeExchangeError', 500, undefined) 116 | this.details = details 117 | } 118 | 119 | toJSON() { 120 | return { 121 | name: this.name, 122 | message: this.message, 123 | status: this.status, 124 | details: this.details, 125 | } 126 | } 127 | } 128 | 129 | export class AuthRetryableFetchError extends CustomAuthError { 130 | constructor(message: string, status: number) { 131 | super(message, 'AuthRetryableFetchError', status, undefined) 132 | } 133 | } 134 | 135 | export function isAuthRetryableFetchError(error: unknown): error is AuthRetryableFetchError { 136 | return isAuthError(error) && error.name === 'AuthRetryableFetchError' 137 | } 138 | 139 | /** 140 | * This error is thrown on certain methods when the password used is deemed 141 | * weak. Inspect the reasons to identify what password strength rules are 142 | * inadequate. 143 | */ 144 | export class AuthWeakPasswordError extends CustomAuthError { 145 | /** 146 | * Reasons why the password is deemed weak. 147 | */ 148 | reasons: WeakPasswordReasons[] 149 | 150 | constructor(message: string, status: number, reasons: string[]) { 151 | super(message, 'AuthWeakPasswordError', status, 'weak_password') 152 | 153 | this.reasons = reasons 154 | } 155 | } 156 | 157 | export function isAuthWeakPasswordError(error: unknown): error is AuthWeakPasswordError { 158 | return isAuthError(error) && error.name === 'AuthWeakPasswordError' 159 | } 160 | 161 | export class AuthInvalidJwtError extends CustomAuthError { 162 | constructor(message: string) { 163 | super(message, 'AuthInvalidJwtError', 400, 'invalid_jwt') 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { API_VERSIONS, API_VERSION_HEADER_NAME } from './constants' 2 | import { expiresAt, looksLikeFetchResponse, parseResponseAPIVersion } from './helpers' 3 | import { 4 | AuthResponse, 5 | AuthResponsePassword, 6 | SSOResponse, 7 | GenerateLinkProperties, 8 | GenerateLinkResponse, 9 | User, 10 | UserResponse, 11 | } from './types' 12 | import { 13 | AuthApiError, 14 | AuthRetryableFetchError, 15 | AuthWeakPasswordError, 16 | AuthUnknownError, 17 | AuthSessionMissingError, 18 | } from './errors' 19 | 20 | export type Fetch = typeof fetch 21 | 22 | export interface FetchOptions { 23 | headers?: { 24 | [key: string]: string 25 | } 26 | noResolveJson?: boolean 27 | } 28 | 29 | export interface FetchParameters { 30 | signal?: AbortSignal 31 | } 32 | 33 | export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'DELETE' 34 | 35 | const _getErrorMessage = (err: any): string => 36 | err.msg || err.message || err.error_description || err.error || JSON.stringify(err) 37 | 38 | const NETWORK_ERROR_CODES = [502, 503, 504] 39 | 40 | export async function handleError(error: unknown) { 41 | if (!looksLikeFetchResponse(error)) { 42 | throw new AuthRetryableFetchError(_getErrorMessage(error), 0) 43 | } 44 | 45 | if (NETWORK_ERROR_CODES.includes(error.status)) { 46 | // status in 500...599 range - server had an error, request might be retryed. 47 | throw new AuthRetryableFetchError(_getErrorMessage(error), error.status) 48 | } 49 | 50 | let data: any 51 | try { 52 | data = await error.json() 53 | } catch (e: any) { 54 | throw new AuthUnknownError(_getErrorMessage(e), e) 55 | } 56 | 57 | let errorCode: string | undefined = undefined 58 | 59 | const responseAPIVersion = parseResponseAPIVersion(error) 60 | if ( 61 | responseAPIVersion && 62 | responseAPIVersion.getTime() >= API_VERSIONS['2024-01-01'].timestamp && 63 | typeof data === 'object' && 64 | data && 65 | typeof data.code === 'string' 66 | ) { 67 | errorCode = data.code 68 | } else if (typeof data === 'object' && data && typeof data.error_code === 'string') { 69 | errorCode = data.error_code 70 | } 71 | 72 | if (!errorCode) { 73 | // Legacy support for weak password errors, when there were no error codes 74 | if ( 75 | typeof data === 'object' && 76 | data && 77 | typeof data.weak_password === 'object' && 78 | data.weak_password && 79 | Array.isArray(data.weak_password.reasons) && 80 | data.weak_password.reasons.length && 81 | data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true) 82 | ) { 83 | throw new AuthWeakPasswordError( 84 | _getErrorMessage(data), 85 | error.status, 86 | data.weak_password.reasons 87 | ) 88 | } 89 | } else if (errorCode === 'weak_password') { 90 | throw new AuthWeakPasswordError( 91 | _getErrorMessage(data), 92 | error.status, 93 | data.weak_password?.reasons || [] 94 | ) 95 | } else if (errorCode === 'session_not_found') { 96 | // The `session_id` inside the JWT does not correspond to a row in the 97 | // `sessions` table. This usually means the user has signed out, has been 98 | // deleted, or their session has somehow been terminated. 99 | throw new AuthSessionMissingError() 100 | } 101 | 102 | throw new AuthApiError(_getErrorMessage(data), error.status || 500, errorCode) 103 | } 104 | 105 | const _getRequestParams = ( 106 | method: RequestMethodType, 107 | options?: FetchOptions, 108 | parameters?: FetchParameters, 109 | body?: object 110 | ) => { 111 | const params: { [k: string]: any } = { method, headers: options?.headers || {} } 112 | 113 | if (method === 'GET') { 114 | return params 115 | } 116 | 117 | params.headers = { 'Content-Type': 'application/json;charset=UTF-8', ...options?.headers } 118 | params.body = JSON.stringify(body) 119 | return { ...params, ...parameters } 120 | } 121 | 122 | interface GotrueRequestOptions extends FetchOptions { 123 | jwt?: string 124 | redirectTo?: string 125 | body?: object 126 | query?: { [key: string]: string } 127 | /** 128 | * Function that transforms api response from gotrue into a desirable / standardised format 129 | */ 130 | xform?: (data: any) => any 131 | } 132 | 133 | export async function _request( 134 | fetcher: Fetch, 135 | method: RequestMethodType, 136 | url: string, 137 | options?: GotrueRequestOptions 138 | ) { 139 | const headers = { 140 | ...options?.headers, 141 | } 142 | 143 | if (!headers[API_VERSION_HEADER_NAME]) { 144 | headers[API_VERSION_HEADER_NAME] = API_VERSIONS['2024-01-01'].name 145 | } 146 | 147 | if (options?.jwt) { 148 | headers['Authorization'] = `Bearer ${options.jwt}` 149 | } 150 | 151 | const qs = options?.query ?? {} 152 | if (options?.redirectTo) { 153 | qs['redirect_to'] = options.redirectTo 154 | } 155 | 156 | const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : '' 157 | const data = await _handleRequest( 158 | fetcher, 159 | method, 160 | url + queryString, 161 | { 162 | headers, 163 | noResolveJson: options?.noResolveJson, 164 | }, 165 | {}, 166 | options?.body 167 | ) 168 | return options?.xform ? options?.xform(data) : { data: { ...data }, error: null } 169 | } 170 | 171 | async function _handleRequest( 172 | fetcher: Fetch, 173 | method: RequestMethodType, 174 | url: string, 175 | options?: FetchOptions, 176 | parameters?: FetchParameters, 177 | body?: object 178 | ): Promise { 179 | const requestParams = _getRequestParams(method, options, parameters, body) 180 | 181 | let result: any 182 | 183 | try { 184 | result = await fetcher(url, { 185 | ...requestParams, 186 | }) 187 | } catch (e) { 188 | console.error(e) 189 | 190 | // fetch failed, likely due to a network or CORS error 191 | throw new AuthRetryableFetchError(_getErrorMessage(e), 0) 192 | } 193 | 194 | if (!result.ok) { 195 | await handleError(result) 196 | } 197 | 198 | if (options?.noResolveJson) { 199 | return result 200 | } 201 | 202 | try { 203 | return await result.json() 204 | } catch (e: any) { 205 | await handleError(e) 206 | } 207 | } 208 | 209 | export function _sessionResponse(data: any): AuthResponse { 210 | let session = null 211 | if (hasSession(data)) { 212 | session = { ...data } 213 | 214 | if (!data.expires_at) { 215 | session.expires_at = expiresAt(data.expires_in) 216 | } 217 | } 218 | 219 | const user: User = data.user ?? (data as User) 220 | return { data: { session, user }, error: null } 221 | } 222 | 223 | export function _sessionResponsePassword(data: any): AuthResponsePassword { 224 | const response = _sessionResponse(data) as AuthResponsePassword 225 | 226 | if ( 227 | !response.error && 228 | data.weak_password && 229 | typeof data.weak_password === 'object' && 230 | Array.isArray(data.weak_password.reasons) && 231 | data.weak_password.reasons.length && 232 | data.weak_password.message && 233 | typeof data.weak_password.message === 'string' && 234 | data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true) 235 | ) { 236 | response.data.weak_password = data.weak_password 237 | } 238 | 239 | return response 240 | } 241 | 242 | export function _userResponse(data: any): UserResponse { 243 | const user: User = data.user ?? (data as User) 244 | return { data: { user }, error: null } 245 | } 246 | 247 | export function _ssoResponse(data: any): SSOResponse { 248 | return { data, error: null } 249 | } 250 | 251 | export function _generateLinkResponse(data: any): GenerateLinkResponse { 252 | const { action_link, email_otp, hashed_token, redirect_to, verification_type, ...rest } = data 253 | 254 | const properties: GenerateLinkProperties = { 255 | action_link, 256 | email_otp, 257 | hashed_token, 258 | redirect_to, 259 | verification_type, 260 | } 261 | 262 | const user: User = { ...rest } 263 | return { 264 | data: { 265 | properties, 266 | user, 267 | }, 268 | error: null, 269 | } 270 | } 271 | 272 | export function _noResolveJsonResponse(data: any): Response { 273 | return data 274 | } 275 | 276 | /** 277 | * hasSession checks if the response object contains a valid session 278 | * @param data A response object 279 | * @returns true if a session is in the response 280 | */ 281 | function hasSession(data: any): boolean { 282 | return data.access_token && data.refresh_token && data.expires_in 283 | } 284 | -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants' 2 | import { AuthInvalidJwtError } from './errors' 3 | import { base64UrlToUint8Array, stringFromBase64URL } from './base64url' 4 | import { JwtHeader, JwtPayload, SupportedStorage, User } from './types' 5 | 6 | export function expiresAt(expiresIn: number) { 7 | const timeNow = Math.round(Date.now() / 1000) 8 | return timeNow + expiresIn 9 | } 10 | 11 | export function uuid() { 12 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 13 | const r = (Math.random() * 16) | 0, 14 | v = c == 'x' ? r : (r & 0x3) | 0x8 15 | return v.toString(16) 16 | }) 17 | } 18 | 19 | export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined' 20 | 21 | const localStorageWriteTests = { 22 | tested: false, 23 | writable: false, 24 | } 25 | 26 | /** 27 | * Checks whether localStorage is supported on this browser. 28 | */ 29 | export const supportsLocalStorage = () => { 30 | if (!isBrowser()) { 31 | return false 32 | } 33 | 34 | try { 35 | if (typeof globalThis.localStorage !== 'object') { 36 | return false 37 | } 38 | } catch (e) { 39 | // DOM exception when accessing `localStorage` 40 | return false 41 | } 42 | 43 | if (localStorageWriteTests.tested) { 44 | return localStorageWriteTests.writable 45 | } 46 | 47 | const randomKey = `lswt-${Math.random()}${Math.random()}` 48 | 49 | try { 50 | globalThis.localStorage.setItem(randomKey, randomKey) 51 | globalThis.localStorage.removeItem(randomKey) 52 | 53 | localStorageWriteTests.tested = true 54 | localStorageWriteTests.writable = true 55 | } catch (e) { 56 | // localStorage can't be written to 57 | // https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document 58 | 59 | localStorageWriteTests.tested = true 60 | localStorageWriteTests.writable = false 61 | } 62 | 63 | return localStorageWriteTests.writable 64 | } 65 | 66 | /** 67 | * Extracts parameters encoded in the URL both in the query and fragment. 68 | */ 69 | export function parseParametersFromURL(href: string) { 70 | const result: { [parameter: string]: string } = {} 71 | 72 | const url = new URL(href) 73 | 74 | if (url.hash && url.hash[0] === '#') { 75 | try { 76 | const hashSearchParams = new URLSearchParams(url.hash.substring(1)) 77 | hashSearchParams.forEach((value, key) => { 78 | result[key] = value 79 | }) 80 | } catch (e: any) { 81 | // hash is not a query string 82 | } 83 | } 84 | 85 | // search parameters take precedence over hash parameters 86 | url.searchParams.forEach((value, key) => { 87 | result[key] = value 88 | }) 89 | 90 | return result 91 | } 92 | 93 | type Fetch = typeof fetch 94 | 95 | export const resolveFetch = (customFetch?: Fetch): Fetch => { 96 | let _fetch: Fetch 97 | if (customFetch) { 98 | _fetch = customFetch 99 | } else if (typeof fetch === 'undefined') { 100 | _fetch = (...args) => 101 | import('@supabase/node-fetch' as any).then(({ default: fetch }) => fetch(...args)) 102 | } else { 103 | _fetch = fetch 104 | } 105 | return (...args) => _fetch(...args) 106 | } 107 | 108 | export const looksLikeFetchResponse = (maybeResponse: unknown): maybeResponse is Response => { 109 | return ( 110 | typeof maybeResponse === 'object' && 111 | maybeResponse !== null && 112 | 'status' in maybeResponse && 113 | 'ok' in maybeResponse && 114 | 'json' in maybeResponse && 115 | typeof (maybeResponse as any).json === 'function' 116 | ) 117 | } 118 | 119 | // Storage helpers 120 | export const setItemAsync = async ( 121 | storage: SupportedStorage, 122 | key: string, 123 | data: any 124 | ): Promise => { 125 | await storage.setItem(key, JSON.stringify(data)) 126 | } 127 | 128 | export const getItemAsync = async (storage: SupportedStorage, key: string): Promise => { 129 | const value = await storage.getItem(key) 130 | 131 | if (!value) { 132 | return null 133 | } 134 | 135 | try { 136 | return JSON.parse(value) 137 | } catch { 138 | return value 139 | } 140 | } 141 | 142 | export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise => { 143 | await storage.removeItem(key) 144 | } 145 | 146 | /** 147 | * A deferred represents some asynchronous work that is not yet finished, which 148 | * may or may not culminate in a value. 149 | * Taken from: https://github.com/mike-north/types/blob/master/src/async.ts 150 | */ 151 | export class Deferred { 152 | public static promiseConstructor: PromiseConstructor = Promise 153 | 154 | public readonly promise!: PromiseLike 155 | 156 | public readonly resolve!: (value?: T | PromiseLike) => void 157 | 158 | public readonly reject!: (reason?: any) => any 159 | 160 | public constructor() { 161 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 162 | ;(this as any).promise = new Deferred.promiseConstructor((res, rej) => { 163 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 164 | ;(this as any).resolve = res 165 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 166 | ;(this as any).reject = rej 167 | }) 168 | } 169 | } 170 | 171 | export function decodeJWT(token: string): { 172 | header: JwtHeader 173 | payload: JwtPayload 174 | signature: Uint8Array 175 | raw: { 176 | header: string 177 | payload: string 178 | } 179 | } { 180 | const parts = token.split('.') 181 | 182 | if (parts.length !== 3) { 183 | throw new AuthInvalidJwtError('Invalid JWT structure') 184 | } 185 | 186 | // Regex checks for base64url format 187 | for (let i = 0; i < parts.length; i++) { 188 | if (!BASE64URL_REGEX.test(parts[i] as string)) { 189 | throw new AuthInvalidJwtError('JWT not in base64url format') 190 | } 191 | } 192 | const data = { 193 | // using base64url lib 194 | header: JSON.parse(stringFromBase64URL(parts[0])), 195 | payload: JSON.parse(stringFromBase64URL(parts[1])), 196 | signature: base64UrlToUint8Array(parts[2]), 197 | raw: { 198 | header: parts[0], 199 | payload: parts[1], 200 | }, 201 | } 202 | return data 203 | } 204 | 205 | /** 206 | * Creates a promise that resolves to null after some time. 207 | */ 208 | export async function sleep(time: number): Promise { 209 | return await new Promise((accept) => { 210 | setTimeout(() => accept(null), time) 211 | }) 212 | } 213 | 214 | /** 215 | * Converts the provided async function into a retryable function. Each result 216 | * or thrown error is sent to the isRetryable function which should return true 217 | * if the function should run again. 218 | */ 219 | export function retryable( 220 | fn: (attempt: number) => Promise, 221 | isRetryable: (attempt: number, error: any | null, result?: T) => boolean 222 | ): Promise { 223 | const promise = new Promise((accept, reject) => { 224 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 225 | ;(async () => { 226 | for (let attempt = 0; attempt < Infinity; attempt++) { 227 | try { 228 | const result = await fn(attempt) 229 | 230 | if (!isRetryable(attempt, null, result)) { 231 | accept(result) 232 | return 233 | } 234 | } catch (e: any) { 235 | if (!isRetryable(attempt, e)) { 236 | reject(e) 237 | return 238 | } 239 | } 240 | } 241 | })() 242 | }) 243 | 244 | return promise 245 | } 246 | 247 | function dec2hex(dec: number) { 248 | return ('0' + dec.toString(16)).substr(-2) 249 | } 250 | 251 | // Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs 252 | export function generatePKCEVerifier() { 253 | const verifierLength = 56 254 | const array = new Uint32Array(verifierLength) 255 | if (typeof crypto === 'undefined') { 256 | const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' 257 | const charSetLen = charSet.length 258 | let verifier = '' 259 | for (let i = 0; i < verifierLength; i++) { 260 | verifier += charSet.charAt(Math.floor(Math.random() * charSetLen)) 261 | } 262 | return verifier 263 | } 264 | crypto.getRandomValues(array) 265 | return Array.from(array, dec2hex).join('') 266 | } 267 | 268 | async function sha256(randomString: string) { 269 | const encoder = new TextEncoder() 270 | const encodedData = encoder.encode(randomString) 271 | const hash = await crypto.subtle.digest('SHA-256', encodedData) 272 | const bytes = new Uint8Array(hash) 273 | 274 | return Array.from(bytes) 275 | .map((c) => String.fromCharCode(c)) 276 | .join('') 277 | } 278 | 279 | export async function generatePKCEChallenge(verifier: string) { 280 | const hasCryptoSupport = 281 | typeof crypto !== 'undefined' && 282 | typeof crypto.subtle !== 'undefined' && 283 | typeof TextEncoder !== 'undefined' 284 | 285 | if (!hasCryptoSupport) { 286 | console.warn( 287 | 'WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.' 288 | ) 289 | return verifier 290 | } 291 | const hashed = await sha256(verifier) 292 | return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 293 | } 294 | 295 | export async function getCodeChallengeAndMethod( 296 | storage: SupportedStorage, 297 | storageKey: string, 298 | isPasswordRecovery = false 299 | ) { 300 | const codeVerifier = generatePKCEVerifier() 301 | let storedCodeVerifier = codeVerifier 302 | if (isPasswordRecovery) { 303 | storedCodeVerifier += '/PASSWORD_RECOVERY' 304 | } 305 | await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier) 306 | const codeChallenge = await generatePKCEChallenge(codeVerifier) 307 | const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256' 308 | return [codeChallenge, codeChallengeMethod] 309 | } 310 | 311 | /** Parses the API version which is 2YYY-MM-DD. */ 312 | const API_VERSION_REGEX = /^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i 313 | 314 | export function parseResponseAPIVersion(response: Response) { 315 | const apiVersion = response.headers.get(API_VERSION_HEADER_NAME) 316 | 317 | if (!apiVersion) { 318 | return null 319 | } 320 | 321 | if (!apiVersion.match(API_VERSION_REGEX)) { 322 | return null 323 | } 324 | 325 | try { 326 | const date = new Date(`${apiVersion}T00:00:00.0Z`) 327 | return date 328 | } catch (e: any) { 329 | return null 330 | } 331 | } 332 | 333 | export function validateExp(exp: number) { 334 | if (!exp) { 335 | throw new Error('Missing exp claim') 336 | } 337 | const timeNow = Math.floor(Date.now() / 1000) 338 | if (exp <= timeNow) { 339 | throw new Error('JWT has expired') 340 | } 341 | } 342 | 343 | export function getAlgorithm(alg: 'RS256' | 'ES256'): RsaHashedImportParams | EcKeyImportParams { 344 | switch (alg) { 345 | case 'RS256': 346 | return { 347 | name: 'RSASSA-PKCS1-v1_5', 348 | hash: { name: 'SHA-256' }, 349 | } 350 | case 'ES256': 351 | return { 352 | name: 'ECDSA', 353 | namedCurve: 'P-256', 354 | hash: { name: 'SHA-256' }, 355 | } 356 | default: 357 | throw new Error('Invalid alg claim') 358 | } 359 | } 360 | 361 | const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ 362 | 363 | export function validateUUID(str: string) { 364 | if (!UUID_REGEX.test(str)) { 365 | throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not') 366 | } 367 | } 368 | 369 | export function userNotAvailableProxy(): User { 370 | const proxyTarget = {} as User 371 | 372 | return new Proxy(proxyTarget, { 373 | get: (target: any, prop: string) => { 374 | if (prop === '__isUserNotAvailableProxy') { 375 | return true 376 | } 377 | // Preventative check for common problematic symbols during cloning/inspection 378 | // These symbols might be accessed by structuredClone or other internal mechanisms. 379 | if (typeof prop === 'symbol') { 380 | const sProp = (prop as symbol).toString() 381 | if ( 382 | sProp === 'Symbol(Symbol.toPrimitive)' || 383 | sProp === 'Symbol(Symbol.toStringTag)' || 384 | sProp === 'Symbol(util.inspect.custom)' 385 | ) { 386 | // Node.js util.inspect 387 | return undefined 388 | } 389 | } 390 | throw new Error( 391 | `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Accessing the "${prop}" property of the session object is not supported. Please use getUser() instead.` 392 | ) 393 | }, 394 | set: (_target: any, prop: string) => { 395 | throw new Error( 396 | `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Setting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.` 397 | ) 398 | }, 399 | deleteProperty: (_target: any, prop: string) => { 400 | throw new Error( 401 | `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Deleting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.` 402 | ) 403 | }, 404 | }) 405 | } 406 | -------------------------------------------------------------------------------- /src/lib/local-storage.ts: -------------------------------------------------------------------------------- 1 | import { SupportedStorage } from './types' 2 | 3 | /** 4 | * Returns a localStorage-like object that stores the key-value pairs in 5 | * memory. 6 | */ 7 | export function memoryLocalStorageAdapter(store: { [key: string]: string } = {}): SupportedStorage { 8 | return { 9 | getItem: (key) => { 10 | return store[key] || null 11 | }, 12 | 13 | setItem: (key, value) => { 14 | store[key] = value 15 | }, 16 | 17 | removeItem: (key) => { 18 | delete store[key] 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/locks.ts: -------------------------------------------------------------------------------- 1 | import { supportsLocalStorage } from './helpers' 2 | 3 | /** 4 | * @experimental 5 | */ 6 | export const internals = { 7 | /** 8 | * @experimental 9 | */ 10 | debug: !!( 11 | globalThis && 12 | supportsLocalStorage() && 13 | globalThis.localStorage && 14 | globalThis.localStorage.getItem('supabase.gotrue-js.locks.debug') === 'true' 15 | ), 16 | } 17 | 18 | /** 19 | * An error thrown when a lock cannot be acquired after some amount of time. 20 | * 21 | * Use the {@link #isAcquireTimeout} property instead of checking with `instanceof`. 22 | */ 23 | export abstract class LockAcquireTimeoutError extends Error { 24 | public readonly isAcquireTimeout = true 25 | 26 | constructor(message: string) { 27 | super(message) 28 | } 29 | } 30 | 31 | export class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {} 32 | export class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {} 33 | 34 | /** 35 | * Implements a global exclusive lock using the Navigator LockManager API. It 36 | * is available on all browsers released after 2022-03-15 with Safari being the 37 | * last one to release support. If the API is not available, this function will 38 | * throw. Make sure you check availablility before configuring {@link 39 | * GoTrueClient}. 40 | * 41 | * You can turn on debugging by setting the `supabase.gotrue-js.locks.debug` 42 | * local storage item to `true`. 43 | * 44 | * Internals: 45 | * 46 | * Since the LockManager API does not preserve stack traces for the async 47 | * function passed in the `request` method, a trick is used where acquiring the 48 | * lock releases a previously started promise to run the operation in the `fn` 49 | * function. The lock waits for that promise to finish (with or without error), 50 | * while the function will finally wait for the result anyway. 51 | * 52 | * @param name Name of the lock to be acquired. 53 | * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if 54 | * the lock can't be acquired without waiting. If positive, the lock acquire 55 | * will time out after so many milliseconds. An error is 56 | * a timeout if it has `isAcquireTimeout` set to true. 57 | * @param fn The operation to run once the lock is acquired. 58 | */ 59 | export async function navigatorLock( 60 | name: string, 61 | acquireTimeout: number, 62 | fn: () => Promise 63 | ): Promise { 64 | if (internals.debug) { 65 | console.log('@supabase/gotrue-js: navigatorLock: acquire lock', name, acquireTimeout) 66 | } 67 | 68 | const abortController = new globalThis.AbortController() 69 | 70 | if (acquireTimeout > 0) { 71 | setTimeout(() => { 72 | abortController.abort() 73 | if (internals.debug) { 74 | console.log('@supabase/gotrue-js: navigatorLock acquire timed out', name) 75 | } 76 | }, acquireTimeout) 77 | } 78 | 79 | // MDN article: https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request 80 | 81 | // Wrapping navigator.locks.request() with a plain Promise is done as some 82 | // libraries like zone.js patch the Promise object to track the execution 83 | // context. However, it appears that most browsers use an internal promise 84 | // implementation when using the navigator.locks.request() API causing them 85 | // to lose context and emit confusing log messages or break certain features. 86 | // This wrapping is believed to help zone.js track the execution context 87 | // better. 88 | return await Promise.resolve().then(() => 89 | globalThis.navigator.locks.request( 90 | name, 91 | acquireTimeout === 0 92 | ? { 93 | mode: 'exclusive', 94 | ifAvailable: true, 95 | } 96 | : { 97 | mode: 'exclusive', 98 | signal: abortController.signal, 99 | }, 100 | async (lock) => { 101 | if (lock) { 102 | if (internals.debug) { 103 | console.log('@supabase/gotrue-js: navigatorLock: acquired', name, lock.name) 104 | } 105 | 106 | try { 107 | return await fn() 108 | } finally { 109 | if (internals.debug) { 110 | console.log('@supabase/gotrue-js: navigatorLock: released', name, lock.name) 111 | } 112 | } 113 | } else { 114 | if (acquireTimeout === 0) { 115 | if (internals.debug) { 116 | console.log('@supabase/gotrue-js: navigatorLock: not immediately available', name) 117 | } 118 | 119 | throw new NavigatorLockAcquireTimeoutError( 120 | `Acquiring an exclusive Navigator LockManager lock "${name}" immediately failed` 121 | ) 122 | } else { 123 | if (internals.debug) { 124 | try { 125 | const result = await globalThis.navigator.locks.query() 126 | 127 | console.log( 128 | '@supabase/gotrue-js: Navigator LockManager state', 129 | JSON.stringify(result, null, ' ') 130 | ) 131 | } catch (e: any) { 132 | console.warn( 133 | '@supabase/gotrue-js: Error when querying Navigator LockManager state', 134 | e 135 | ) 136 | } 137 | } 138 | 139 | // Browser is not following the Navigator LockManager spec, it 140 | // returned a null lock when we didn't use ifAvailable. So we can 141 | // pretend the lock is acquired in the name of backward compatibility 142 | // and user experience and just run the function. 143 | console.warn( 144 | '@supabase/gotrue-js: Navigator LockManager returned a null lock when using #request without ifAvailable set to true, it appears this browser is not following the LockManager spec https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request' 145 | ) 146 | 147 | return await fn() 148 | } 149 | } 150 | } 151 | ) 152 | ) 153 | } 154 | 155 | const PROCESS_LOCKS: { [name: string]: Promise } = {} 156 | 157 | /** 158 | * Implements a global exclusive lock that works only in the current process. 159 | * Useful for environments like React Native or other non-browser 160 | * single-process (i.e. no concept of "tabs") environments. 161 | * 162 | * Use {@link #navigatorLock} in browser environments. 163 | * 164 | * @param name Name of the lock to be acquired. 165 | * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if 166 | * the lock can't be acquired without waiting. If positive, the lock acquire 167 | * will time out after so many milliseconds. An error is 168 | * a timeout if it has `isAcquireTimeout` set to true. 169 | * @param fn The operation to run once the lock is acquired. 170 | */ 171 | export async function processLock( 172 | name: string, 173 | acquireTimeout: number, 174 | fn: () => Promise 175 | ): Promise { 176 | const previousOperation = PROCESS_LOCKS[name] ?? Promise.resolve() 177 | 178 | const currentOperation = Promise.race( 179 | [ 180 | previousOperation.catch(() => { 181 | // ignore error of previous operation that we're waiting to finish 182 | return null 183 | }), 184 | acquireTimeout >= 0 185 | ? new Promise((_, reject) => { 186 | setTimeout(() => { 187 | reject( 188 | new ProcessLockAcquireTimeoutError( 189 | `Acquring process lock with name "${name}" timed out` 190 | ) 191 | ) 192 | }, acquireTimeout) 193 | }) 194 | : null, 195 | ].filter((x) => x) 196 | ) 197 | .catch((e: any) => { 198 | if (e && e.isAcquireTimeout) { 199 | throw e 200 | } 201 | 202 | return null 203 | }) 204 | .then(async () => { 205 | // previous operations finished and we didn't get a race on the acquire 206 | // timeout, so the current operation can finally start 207 | return await fn() 208 | }) 209 | 210 | PROCESS_LOCKS[name] = currentOperation.catch(async (e: any) => { 211 | if (e && e.isAcquireTimeout) { 212 | // if the current operation timed out, it doesn't mean that the previous 213 | // operation finished, so we need contnue waiting for it to finish 214 | await previousOperation 215 | 216 | return null 217 | } 218 | 219 | throw e 220 | }) 221 | 222 | // finally wait for the current operation to finish successfully, with an 223 | // error or with an acquire timeout error 224 | return await currentOperation 225 | } 226 | -------------------------------------------------------------------------------- /src/lib/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://mathiasbynens.be/notes/globalthis 3 | */ 4 | export function polyfillGlobalThis() { 5 | if (typeof globalThis === 'object') return 6 | try { 7 | Object.defineProperty(Object.prototype, '__magic__', { 8 | get: function () { 9 | return this 10 | }, 11 | configurable: true, 12 | }) 13 | // @ts-expect-error 'Allow access to magic' 14 | __magic__.globalThis = __magic__ 15 | // @ts-expect-error 'Allow access to magic' 16 | delete Object.prototype.__magic__ 17 | } catch (e) { 18 | if (typeof self !== 'undefined') { 19 | // @ts-expect-error 'Allow access to globals' 20 | self.globalThis = self 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/version.ts: -------------------------------------------------------------------------------- 1 | // Generated by genversion. 2 | export const version = '0.0.0' 3 | -------------------------------------------------------------------------------- /test/GoTrueApi.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | authClientWithSession, 3 | clientApiAutoConfirmOffSignupsEnabledClient, 4 | serviceRoleApiClient, 5 | clientApiAutoConfirmDisabledClient, 6 | } from './lib/clients' 7 | 8 | import { 9 | createNewUserWithEmail, 10 | mockUserCredentials, 11 | mockAppMetadata, 12 | mockUserMetadata, 13 | mockVerificationOTP, 14 | } from './lib/utils' 15 | 16 | import type { GenerateLinkProperties, User } from '../src/lib/types' 17 | 18 | const INVALID_EMAIL = 'xx:;x@x.x' 19 | const NON_EXISTANT_USER_ID = '83fd9e20-7a80-46e4-bf29-a86e3d6bbf66' 20 | 21 | describe('GoTrueAdminApi', () => { 22 | describe('User creation', () => { 23 | test('createUser() should create a new user', async () => { 24 | const { email } = mockUserCredentials() 25 | const { error, data } = await createNewUserWithEmail({ email }) 26 | 27 | expect(error).toBeNull() 28 | expect(data.user?.email).toEqual(email) 29 | }) 30 | 31 | test('createUser() with user metadata', async () => { 32 | const user_metadata = mockUserMetadata() 33 | const { email, password } = mockUserCredentials() 34 | 35 | const { error, data } = await serviceRoleApiClient.createUser({ 36 | email, 37 | password, 38 | user_metadata, 39 | }) 40 | 41 | expect(error).toBeNull() 42 | expect(data.user?.email).toEqual(email) 43 | expect(data.user?.user_metadata).toEqual(user_metadata) 44 | expect(data.user?.user_metadata).toHaveProperty('profile_image') 45 | expect(data.user?.user_metadata?.profile_image).toMatch(/https.*avatars.*(jpg|png)/) 46 | }) 47 | 48 | test('createUser() with app metadata', async () => { 49 | const app_metadata = mockAppMetadata() 50 | const { email, password } = mockUserCredentials() 51 | 52 | const { error, data } = await serviceRoleApiClient.createUser({ 53 | email, 54 | password, 55 | app_metadata, 56 | }) 57 | 58 | expect(error).toBeNull() 59 | expect(data.user?.email).toEqual(email) 60 | expect(data.user?.app_metadata).toHaveProperty('provider') 61 | expect(data.user?.app_metadata).toHaveProperty('providers') 62 | }) 63 | 64 | test('createUser() with user and app metadata', async () => { 65 | const user_metadata = mockUserMetadata() 66 | const app_metadata = mockAppMetadata() 67 | 68 | const { email, password } = mockUserCredentials() 69 | 70 | const { error, data } = await serviceRoleApiClient.createUser({ 71 | email, 72 | password, 73 | app_metadata, 74 | user_metadata, 75 | }) 76 | 77 | expect(error).toBeNull() 78 | expect(data.user?.email).toEqual(email) 79 | expect(data.user?.user_metadata).toHaveProperty('profile_image') 80 | expect(data.user?.user_metadata?.profile_image).toMatch(/https.*avatars.*(jpg|png)/) 81 | expect(data.user?.app_metadata).toHaveProperty('provider') 82 | expect(data.user?.app_metadata).toHaveProperty('providers') 83 | }) 84 | 85 | test('createUser() returns AuthError when email is invalid', async () => { 86 | const { error, data } = await serviceRoleApiClient.createUser({ 87 | email: INVALID_EMAIL, 88 | password: 'password123', 89 | }) 90 | 91 | expect(error).not.toBeNull() 92 | expect(error?.message).toMatch('Unable to validate email address: invalid format') 93 | expect(data.user).toBeNull() 94 | }) 95 | }) 96 | 97 | describe('User fetch', () => { 98 | test('listUsers() should return registered users', async () => { 99 | const { email } = mockUserCredentials() 100 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 101 | expect(createError).toBeNull() 102 | expect(createdUser.user).not.toBeUndefined() 103 | 104 | const { error: listUserError, data: userList } = await serviceRoleApiClient.listUsers() 105 | expect(listUserError).toBeNull() 106 | expect(userList).toHaveProperty('users') 107 | expect(userList).toHaveProperty('aud') 108 | const emails = 109 | userList.users?.map((user: User) => { 110 | return user.email 111 | }) || [] 112 | 113 | expect(emails.length).toBeGreaterThan(0) 114 | expect(emails).toContain(email) 115 | }) 116 | 117 | test('getUser() fetches a user by their access_token', async () => { 118 | const { email, password } = mockUserCredentials() 119 | const { error: initialError, data } = await authClientWithSession.signUp({ 120 | email, 121 | password, 122 | }) 123 | 124 | expect(initialError).toBeNull() 125 | expect(data.session).not.toBeNull() 126 | 127 | const { 128 | error, 129 | data: { user }, 130 | } = await authClientWithSession.getUser() 131 | 132 | expect(error).toBeNull() 133 | expect(user).not.toBeUndefined() 134 | expect(user?.email).toEqual(email) 135 | }) 136 | 137 | test('getUserById() should a registered user given its user identifier', async () => { 138 | const { email } = mockUserCredentials() 139 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 140 | 141 | expect(createError).toBeNull() 142 | expect(createdUser.user).not.toBeUndefined() 143 | 144 | const uid = createdUser.user?.id || '' 145 | expect(uid).toBeTruthy() 146 | 147 | const { error: foundError, data: foundUser } = await serviceRoleApiClient.getUserById(uid) 148 | 149 | expect(foundError).toBeNull() 150 | expect(foundUser).not.toBeUndefined() 151 | expect(foundUser.user?.email).toEqual(email) 152 | }) 153 | 154 | test('getUserById() returns AuthError when user id is invalid', async () => { 155 | const { error, data } = await serviceRoleApiClient.getUserById(NON_EXISTANT_USER_ID) 156 | 157 | expect(error).not.toBeNull() 158 | expect(data.user).toBeNull() 159 | }) 160 | }) 161 | 162 | describe('User updates', () => { 163 | test('modify email using updateUserById()', async () => { 164 | const { email } = mockUserCredentials() 165 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 166 | 167 | expect(createError).toBeNull() 168 | expect(createdUser.user).not.toBeUndefined() 169 | 170 | const uid = createdUser.user?.id || '' 171 | 172 | const attributes = { email: `new_${createdUser.user?.email}` } 173 | 174 | const { error: updatedError, data: updatedUser } = await serviceRoleApiClient.updateUserById( 175 | uid, 176 | attributes 177 | ) 178 | 179 | expect(updatedError).toBeNull() 180 | expect(updatedError).not.toBeUndefined() 181 | expect(updatedUser.user?.email).toEqual(`new_${createdUser.user?.email}`) 182 | }) 183 | 184 | test('modify user metadata using updateUserById()', async () => { 185 | const { email } = mockUserCredentials() 186 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 187 | 188 | expect(createError).toBeNull() 189 | expect(createdUser.user).not.toBeUndefined() 190 | 191 | const uid = createdUser.user?.id || '' 192 | 193 | const userMetaData = { favorite_color: 'yellow' } 194 | const attributes = { user_metadata: userMetaData } 195 | 196 | const { error: updatedError, data: updatedUser } = await serviceRoleApiClient.updateUserById( 197 | uid, 198 | attributes 199 | ) 200 | 201 | expect(updatedError).toBeNull() 202 | expect(updatedError).not.toBeUndefined() 203 | expect(updatedUser.user?.email).toEqual(email) 204 | expect(updatedUser.user?.user_metadata).toEqual(userMetaData) 205 | }) 206 | 207 | test('modify app metadata using updateUserById()', async () => { 208 | const { email } = mockUserCredentials() 209 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 210 | 211 | expect(createError).toBeNull() 212 | expect(createdUser.user).not.toBeUndefined() 213 | 214 | const uid = createdUser.user?.id || '' 215 | const appMetadata = { roles: ['admin', 'publisher'] } 216 | const attributes = { app_metadata: appMetadata } 217 | const { error: updatedError, data: updatedUser } = await serviceRoleApiClient.updateUserById( 218 | uid, 219 | attributes 220 | ) 221 | 222 | expect(updatedError).toBeNull() 223 | expect(updatedError).not.toBeUndefined() 224 | expect(updatedUser.user?.email).toEqual(email) 225 | expect(updatedUser.user?.app_metadata).toHaveProperty('roles') 226 | }) 227 | 228 | test('modify confirm email using updateUserById()', async () => { 229 | const { email, password } = mockUserCredentials() 230 | const { error: createError, data } = await clientApiAutoConfirmOffSignupsEnabledClient.signUp( 231 | { 232 | email, 233 | password, 234 | } 235 | ) 236 | 237 | expect(createError).toBeNull() 238 | expect(data.user).not.toBeUndefined() 239 | expect(data.user).not.toHaveProperty('email_confirmed_at') 240 | expect(data.user?.email_confirmed_at).toBeFalsy() 241 | 242 | const uid = data.user?.id || '' 243 | 244 | const attributes = { email_confirm: true } 245 | const { error: updatedError, data: updatedUser } = await serviceRoleApiClient.updateUserById( 246 | uid, 247 | attributes 248 | ) 249 | 250 | expect(updatedError).toBeNull() 251 | expect(updatedUser).not.toBeUndefined() 252 | expect(updatedUser.user).toHaveProperty('email_confirmed_at') 253 | expect(updatedUser.user?.email_confirmed_at).toBeTruthy() 254 | }) 255 | }) 256 | 257 | describe('User deletes', () => { 258 | test('deleteUser() should be able delete an existing user', async () => { 259 | const { email } = mockUserCredentials() 260 | const { error: createError, data: createdUser } = await createNewUserWithEmail({ email }) 261 | 262 | expect(createError).toBeNull() 263 | expect(createdUser.user).not.toBeUndefined() 264 | 265 | const uid = createdUser.user?.id || '' 266 | 267 | const { error: deletedError, data: deletedUser } = await serviceRoleApiClient.deleteUser(uid) 268 | 269 | expect(deletedError).toBeNull() 270 | expect(deletedError).not.toBeUndefined() 271 | expect(deletedUser.user).not.toBeUndefined() 272 | 273 | const { error: listUserError, data } = await serviceRoleApiClient.listUsers() 274 | expect(listUserError).toBeNull() 275 | 276 | const emails = 277 | data.users?.map((user) => { 278 | return user.email 279 | }) || [] 280 | 281 | expect(emails.length).toBeGreaterThan(0) 282 | expect(emails).not.toContain(email) 283 | }) 284 | 285 | test('deleteUser() returns AuthError when user id is invalid', async () => { 286 | const { error, data } = await serviceRoleApiClient.deleteUser(NON_EXISTANT_USER_ID) 287 | 288 | expect(error).not.toBeNull() 289 | expect(data.user).toBeNull() 290 | }) 291 | }) 292 | 293 | describe('User registration', () => { 294 | test('generateLink supports signUp with generate confirmation signup link', async () => { 295 | const { email, password } = mockUserCredentials() 296 | 297 | const redirectTo = 'http://localhost:9999/welcome' 298 | const userMetadata = { status: 'alpha' } 299 | 300 | const { error, data } = await serviceRoleApiClient.generateLink({ 301 | type: 'signup', 302 | email, 303 | password: password, 304 | options: { 305 | data: userMetadata, 306 | redirectTo, 307 | }, 308 | }) 309 | 310 | const properties = data.properties as GenerateLinkProperties 311 | const user = data.user as User 312 | 313 | expect(error).toBeNull() 314 | /** Check that the user object returned has the update metadata and an email */ 315 | expect(user).not.toBeNull() 316 | expect(user).toHaveProperty('email') 317 | expect(user).toHaveProperty('user_metadata') 318 | expect(user?.['user_metadata']).toEqual(userMetadata) 319 | 320 | /** Check that properties returned contains the generateLink properties */ 321 | expect(properties).not.toBeNull() 322 | expect(properties).toHaveProperty('action_link') 323 | expect(properties).toHaveProperty('email_otp') 324 | expect(properties).toHaveProperty('hashed_token') 325 | expect(properties).toHaveProperty('redirect_to') 326 | expect(properties).toHaveProperty('verification_type') 327 | 328 | /** Check if the action link returned is correctly formatted */ 329 | expect(properties?.['action_link']).toMatch(/\?token/) 330 | expect(properties?.['action_link']).toMatch(/type=signup/) 331 | expect(properties?.['action_link']).toMatch(new RegExp(`redirect_to=${redirectTo}`)) 332 | }) 333 | 334 | test('generateLink supports updating emails with generate email change links', async () => { 335 | const { email } = mockUserCredentials() 336 | const { data: createdUser, error: createError } = await createNewUserWithEmail({ 337 | email, 338 | }) 339 | expect(createError).toBeNull() 340 | expect(createdUser).not.toBeNull() 341 | 342 | const { email: newEmail } = mockUserCredentials() 343 | const redirectTo = 'http://localhost:9999/welcome' 344 | 345 | const { data, error } = await serviceRoleApiClient.generateLink({ 346 | type: 'email_change_current', 347 | email, 348 | newEmail, 349 | options: { 350 | redirectTo, 351 | }, 352 | }) 353 | const properties = data.properties as GenerateLinkProperties 354 | const user = data.user as User 355 | 356 | expect(error).toBeNull() 357 | /** Check that the user object returned has the update metadata and an email */ 358 | expect(user).not.toBeNull() 359 | expect(user).toHaveProperty('email') 360 | expect(user).toHaveProperty('new_email') 361 | expect(user).toHaveProperty('user_metadata') 362 | expect(user?.new_email).toEqual(newEmail) 363 | 364 | /** Check that properties returned contains the generateLink properties */ 365 | expect(properties).not.toBeNull() 366 | expect(properties).toHaveProperty('action_link') 367 | expect(properties).toHaveProperty('email_otp') 368 | expect(properties).toHaveProperty('hashed_token') 369 | expect(properties).toHaveProperty('redirect_to') 370 | expect(properties).toHaveProperty('verification_type') 371 | 372 | /** Check if the action link returned is correctly formatted */ 373 | expect(properties?.['action_link']).toMatch(/\?token/) 374 | expect(properties?.['action_link']).toMatch(/type=email_change/) 375 | expect(properties?.['action_link']).toMatch(new RegExp(`redirect_to=${redirectTo}`)) 376 | }) 377 | 378 | test('inviteUserByEmail() creates a new user with an invited_at timestamp', async () => { 379 | const { email } = mockUserCredentials() 380 | 381 | const redirectTo = 'http://localhost:9999/welcome' 382 | const userMetadata = { status: 'alpha' } 383 | const { error, data } = await serviceRoleApiClient.inviteUserByEmail(email, { 384 | data: userMetadata, 385 | redirectTo, 386 | }) 387 | 388 | expect(error).toBeNull() 389 | expect(data.user).not.toBeNull() 390 | expect(data.user).toHaveProperty('invited_at') 391 | expect(data.user?.invited_at).toBeDefined() 392 | }) 393 | 394 | test('inviteUserByEmail() returns AuthError when email is invalid', async () => { 395 | const { error, data } = await serviceRoleApiClient.inviteUserByEmail(INVALID_EMAIL) 396 | 397 | expect(error).not.toBeNull() 398 | expect(error?.message).toMatch('Unable to validate email address: invalid format') 399 | expect(data.user).toBeNull() 400 | }) 401 | 402 | test('generateLink() returns AuthError when email is invalid', async () => { 403 | const { error, data } = await serviceRoleApiClient.generateLink({ 404 | type: 'signup', 405 | email: INVALID_EMAIL, 406 | password: 'password123', 407 | }) 408 | 409 | expect(error).not.toBeNull() 410 | expect(error?.message).toMatch('Unable to validate email address: invalid format') 411 | expect(data.user).toBeNull() 412 | expect(data.properties).toBeNull() 413 | }) 414 | }) 415 | 416 | describe('User authentication', () => { 417 | describe('signOut()', () => { 418 | test('signOut() with an valid access token', async () => { 419 | const { email, password } = mockUserCredentials() 420 | 421 | const { error: initialError, data } = await authClientWithSession.signUp({ 422 | email, 423 | password, 424 | }) 425 | 426 | expect(initialError).toBeNull() 427 | expect(data.session).not.toBeNull() 428 | 429 | const { error } = await serviceRoleApiClient.signOut(data.session?.access_token || '') 430 | expect(error).toBeNull() 431 | }) 432 | 433 | test('signOut() with an invalid access token', async () => { 434 | const { error } = await serviceRoleApiClient.signOut('this-is-a-bad-token') 435 | 436 | expect(error?.message).toMatch(/^invalid JWT/) 437 | }) 438 | }) 439 | }) 440 | 441 | describe('Email/Phone Otp Verification', () => { 442 | describe('GoTrueClient verifyOtp()', () => { 443 | test('verifyOtp() with non-existent phone number', async () => { 444 | const { phone } = mockUserCredentials() 445 | const otp = mockVerificationOTP() 446 | const { 447 | data: { user }, 448 | error, 449 | } = await clientApiAutoConfirmDisabledClient.verifyOtp({ 450 | phone: `${phone}`, 451 | token: otp, 452 | type: 'sms', 453 | }) 454 | 455 | expect(user).toBeNull() 456 | expect(error?.message).toEqual('Token has expired or is invalid') 457 | expect(error?.status).toEqual(403) 458 | }) 459 | 460 | test('verifyOTP() with invalid phone number', async () => { 461 | const { phone } = mockUserCredentials() 462 | const otp = mockVerificationOTP() 463 | const { 464 | data: { user }, 465 | error, 466 | } = await clientApiAutoConfirmDisabledClient.verifyOtp({ 467 | phone: `${phone}-invalid`, 468 | token: otp, 469 | type: 'sms', 470 | }) 471 | 472 | expect(user).toBeNull() 473 | expect(error?.message).toEqual('Invalid phone number format (E.164 required)') 474 | }) 475 | }) 476 | }) 477 | 478 | describe('List Users', () => { 479 | test('listUsers() returns AuthError when page is invalid', async () => { 480 | const { error, data } = await serviceRoleApiClient.listUsers({ 481 | page: -1, 482 | perPage: 10, 483 | }) 484 | 485 | expect(error).not.toBeNull() 486 | expect(data.users).toEqual([]) 487 | }) 488 | }) 489 | 490 | describe('Update User', () => { 491 | test('updateUserById() returns AuthError when user id is invalid', async () => { 492 | const { error, data } = await serviceRoleApiClient.updateUserById(NON_EXISTANT_USER_ID, { 493 | email: 'new@email.com', 494 | }) 495 | 496 | expect(error).not.toBeNull() 497 | expect(data.user).toBeNull() 498 | }) 499 | }) 500 | 501 | describe('MFA Admin', () => { 502 | test('mfa factor management: add, list and delete', async () => { 503 | const { email, password } = mockUserCredentials() 504 | 505 | const { error: signUpError, data: signUpData } = await authClientWithSession.signUp({ 506 | email, 507 | password, 508 | }) 509 | expect(signUpError).toBeNull() 510 | expect(signUpData.session).not.toBeNull() 511 | 512 | const uid = signUpData.user?.id || '' 513 | expect(uid).toBeTruthy() 514 | 515 | const { error: enrollError } = await authClientWithSession.mfa.enroll({ 516 | factorType: 'totp', 517 | }) 518 | expect(enrollError).toBeNull() 519 | 520 | const { data, error } = await serviceRoleApiClient.mfa.listFactors({ userId: uid }) 521 | 522 | expect(error).toBeNull() 523 | expect(data).not.toBeNull() 524 | expect(Array.isArray(data?.factors)).toBe(true) 525 | expect(data?.factors.length).toBeGreaterThan(0) 526 | 527 | const factorId = data?.factors[0].id 528 | expect(factorId).toBeDefined() 529 | const { data: deletedData, error: deletedError } = 530 | await serviceRoleApiClient.mfa.deleteFactor({ 531 | userId: uid, 532 | id: factorId!, 533 | }) 534 | expect(deletedError).toBeNull() 535 | expect(deletedData).not.toBeNull() 536 | const deletedId = (deletedData as any)?.data?.id 537 | console.log('deletedId:', deletedId) 538 | expect(deletedId).toEqual(factorId) 539 | 540 | const { data: latestData, error: latestError } = await serviceRoleApiClient.mfa.listFactors({ 541 | userId: uid, 542 | }) 543 | expect(latestError).toBeNull() 544 | expect(latestData).not.toBeNull() 545 | expect(Array.isArray(latestData?.factors)).toBe(true) 546 | expect(latestData?.factors.length).toEqual(0) 547 | }) 548 | 549 | test('mfa.listFactors returns AuthError for invalid user', async () => { 550 | const { data, error } = await serviceRoleApiClient.mfa.listFactors({ 551 | userId: NON_EXISTANT_USER_ID, 552 | }) 553 | expect(data).toBeNull() 554 | expect(error).not.toBeNull() 555 | }) 556 | 557 | test('mfa.deleteFactors returns AuthError for invalid user', async () => { 558 | const { data, error } = await serviceRoleApiClient.mfa.deleteFactor({ 559 | userId: NON_EXISTANT_USER_ID, 560 | id: NON_EXISTANT_USER_ID, 561 | }) 562 | expect(data).toBeNull() 563 | expect(error).not.toBeNull() 564 | }) 565 | }) 566 | }) 567 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Prerequisites 4 | 5 | - Docker & docker compose 6 | 7 | ### Basic testing 8 | 9 | Run all tests: 10 | 11 | ```sh 12 | npm run test 13 | ``` 14 | 15 | > Note: If tests fail due to connection issues, your tests may be running too soon and the infra is not yet ready. 16 | > If that's the case, adjust the `sleep 10` duration in: 17 | > `"test:infra": "cd infra && docker-compose down && docker-compose pull && docker-compose up -d && sleep 10",` 18 | > to a value that works for your system setup. 19 | 20 | ### Advanced 21 | 22 | **Start all the infrastructure:** 23 | 24 | ```sh 25 | npm run test:infra 26 | ``` 27 | 28 | You can now open the mock mail server on `http://localhost:9000` 29 | 30 | **Run the tests only:** 31 | 32 | ```sh 33 | npm run test:suite 34 | ``` 35 | 36 | All emails will appear in the mock mail server. 37 | -------------------------------------------------------------------------------- /test/base64url.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | codepointToUTF8, 3 | stringFromBase64URL, 4 | stringFromUTF8, 5 | stringToBase64URL, 6 | } from '../src/lib/base64url' 7 | 8 | const EXAMPLES = [ 9 | 'a', 10 | 'ab', 11 | 'abc', 12 | 'abcd', 13 | 'hello world', 14 | 'нешто на кирилица', 15 | 'something with emojis 🤙🏾 ', 16 | 'Supabaseは、オープンソースの Firebase 代替製品です。エンタープライズグレードのオープンソースツールを使って、Firebase の機能を構築しています。', 17 | ] 18 | 19 | describe('stringToBase64URL', () => { 20 | EXAMPLES.forEach((example) => { 21 | test(`encode "${example}"`, () => { 22 | expect(stringToBase64URL(example)).toEqual(Buffer.from(example).toString('base64url')) 23 | }) 24 | }) 25 | }) 26 | 27 | describe('stringFromBase64URL', () => { 28 | EXAMPLES.forEach((example) => { 29 | test(`decode "${example}"`, () => { 30 | expect(stringFromBase64URL('\r\t\n ' + Buffer.from(example).toString('base64url'))).toEqual( 31 | example 32 | ) 33 | }) 34 | }) 35 | 36 | test('decode with invalid Base64-URL character', () => { 37 | expect(() => { 38 | stringFromBase64URL('*') 39 | }).toThrow(new Error(`Invalid Base64-URL character "*"`)) 40 | }) 41 | }) 42 | 43 | const BAD_UTF8 = [ 44 | [0xf8], // 11111000 45 | [0xff], // 11111111 46 | [0x80], // 10000000 47 | [0xf8, 1], // 11110000 00000001 48 | [0xe0, 1], // 11100000 00000001 49 | [0xc0, 1], // 11100000 00000001 50 | ] 51 | 52 | describe('stringFromUTF8', () => { 53 | BAD_UTF8.forEach((example) => { 54 | test(`should recognize bad UTF-8 sequence ${example 55 | .map((x) => x.toString(16)) 56 | .join(' ')}`, () => { 57 | expect(() => { 58 | const state = { utf8seq: 0, codepoint: 0 } 59 | example.forEach((byte) => { 60 | // eslint-disable-next-line @typescript-eslint/no-empty-function 61 | stringFromUTF8(byte, state, () => {}) 62 | }) 63 | }).toThrow(new Error('Invalid UTF-8 sequence')) 64 | }) 65 | }) 66 | }) 67 | 68 | describe('codepointToUTF8', () => { 69 | test('invalid codepoints above 0x10ffff', () => { 70 | const invalidCodepoint = 0x10ffff + 1 71 | expect(() => { 72 | codepointToUTF8(invalidCodepoint, () => { 73 | throw new Error('Should not becalled') 74 | }) 75 | }).toThrow(new Error(`Unrecognized Unicode codepoint: ${invalidCodepoint.toString(16)}`)) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { MockServer } from 'jest-mock-server' 2 | // @ts-ignore 3 | import fetch from '@supabase/node-fetch' 4 | import { API_VERSION_HEADER_NAME } from '../src/lib/constants' 5 | import { AuthUnknownError, AuthApiError, AuthRetryableFetchError } from '../src/lib/errors' 6 | import { _request, handleError } from '../src/lib/fetch' 7 | 8 | describe('fetch', () => { 9 | const server = new MockServer() 10 | 11 | beforeAll(async () => await server.start()) 12 | afterAll(async () => await server.stop()) 13 | beforeEach(() => server.reset()) 14 | 15 | describe('Error handling', () => { 16 | test('should throw AuthApiError if the error response does contain valid json', async () => { 17 | const route = server 18 | .get('/') 19 | .mockImplementationOnce((ctx) => { 20 | ctx.status = 400 21 | ctx.body = { message: 'invalid params' } 22 | }) 23 | .mockImplementation((ctx) => { 24 | ctx.status = 200 25 | }) 26 | 27 | const url = server.getURL().toString() 28 | 29 | await expect(_request(fetch, 'GET', url)).rejects.toBeInstanceOf(AuthApiError) 30 | 31 | expect(route).toHaveBeenCalledTimes(1) 32 | }) 33 | 34 | test('should throw AuthUnknownError if the error response does not contain valid json', async () => { 35 | const route = server 36 | .get('/') 37 | .mockImplementationOnce((ctx) => { 38 | ctx.status = 400 39 | ctx.body = 'Bad Request' 40 | }) 41 | .mockImplementation((ctx) => { 42 | ctx.status = 200 43 | }) 44 | 45 | const url = server.getURL().toString() 46 | 47 | await expect(_request(fetch, 'GET', url)).rejects.toBeInstanceOf(AuthUnknownError) 48 | 49 | expect(route).toHaveBeenCalledTimes(1) 50 | }) 51 | 52 | test('should not throw AuthRetryableFetchError upon internal server error', async () => { 53 | const route = server 54 | .get('/') 55 | .mockImplementationOnce((ctx) => { 56 | ctx.status = 500 57 | ctx.body = 'Internal Server Error' 58 | }) 59 | .mockImplementation((ctx) => { 60 | ctx.status = 200 61 | }) 62 | 63 | const url = server.getURL().toString() 64 | 65 | await expect(_request(fetch, 'GET', url)).rejects.not.toBeInstanceOf(AuthRetryableFetchError) 66 | 67 | expect(route).toHaveBeenCalledTimes(1) 68 | }) 69 | 70 | test('should throw AuthRetryableFetchError when the server aborts the request without sending any response', async () => { 71 | const route = server 72 | .get('/') 73 | .mockImplementationOnce((ctx) => { 74 | // abort the request without sending any response 75 | ctx.req.destroy() 76 | }) 77 | .mockImplementation((ctx) => { 78 | ctx.status = 200 79 | }) 80 | 81 | const url = server.getURL().toString() 82 | 83 | await expect(_request(fetch, 'GET', url)).rejects.toBeInstanceOf(AuthRetryableFetchError) 84 | 85 | expect(route).toHaveBeenCalledTimes(1) 86 | }) 87 | 88 | test('should throw AuthRetryableFetchError when the server is not reachable', async () => { 89 | const route = server 90 | .get('/') 91 | .mockImplementationOnce((ctx) => { 92 | ctx.status = 500 93 | ctx.body = 'Internal Server Error' 94 | }) 95 | .mockImplementation((ctx) => { 96 | ctx.status = 200 97 | }) 98 | 99 | const url = server.getURL().toString() 100 | await server.stop() 101 | 102 | await expect(_request(fetch, 'GET', url)).rejects.toBeInstanceOf(AuthRetryableFetchError) 103 | 104 | expect(route).toHaveBeenCalledTimes(0) 105 | 106 | await server.start() 107 | }) 108 | 109 | test('should work with custom fetch implementation', async () => { 110 | const customFetch = (async () => { 111 | return { 112 | status: 400, 113 | ok: false, 114 | json: async () => { 115 | return { message: 'invalid params' } 116 | }, 117 | headers: { 118 | get: () => { 119 | return null 120 | }, 121 | }, 122 | } 123 | }) as unknown as typeof fetch 124 | 125 | const url = server.getURL().toString() 126 | 127 | await expect(_request(customFetch, 'GET', url)).rejects.toBeInstanceOf(AuthApiError) 128 | }) 129 | }) 130 | }) 131 | 132 | describe('handleError', () => { 133 | ;[ 134 | { 135 | name: 'without API version and error code', 136 | code: 'error_code', 137 | ename: 'AuthApiError', 138 | response: new Response( 139 | JSON.stringify({ 140 | code: 400, 141 | msg: 'Error code message', 142 | error_code: 'error_code', 143 | }), 144 | { 145 | status: 400, 146 | statusText: 'Bad Request', 147 | } 148 | ), 149 | }, 150 | { 151 | name: 'without API version and weak password error code with payload', 152 | code: 'weak_password', 153 | ename: 'AuthWeakPasswordError', 154 | response: new Response( 155 | JSON.stringify({ 156 | code: 400, 157 | msg: 'Error code message', 158 | error_code: 'weak_password', 159 | weak_password: { 160 | reasons: ['characters'], 161 | }, 162 | }), 163 | { 164 | status: 400, 165 | statusText: 'Bad Request', 166 | } 167 | ), 168 | }, 169 | { 170 | name: 'without API version, no error code and weak_password payload', 171 | code: 'weak_password', 172 | ename: 'AuthWeakPasswordError', 173 | response: new Response( 174 | JSON.stringify({ 175 | msg: 'Error code message', 176 | weak_password: { 177 | reasons: ['characters'], 178 | }, 179 | }), 180 | { 181 | status: 400, 182 | statusText: 'Bad Request', 183 | } 184 | ), 185 | }, 186 | { 187 | name: 'with API version 2024-01-01 and error code', 188 | code: 'error_code', 189 | ename: 'AuthApiError', 190 | response: new Response( 191 | JSON.stringify({ 192 | code: 'error_code', 193 | message: 'Error code message', 194 | }), 195 | { 196 | status: 400, 197 | statusText: 'Bad Request', 198 | headers: { 199 | [API_VERSION_HEADER_NAME]: '2024-01-01', 200 | }, 201 | } 202 | ), 203 | }, 204 | ].forEach((example) => { 205 | it(`should handle error response ${example.name}`, async () => { 206 | let error: any = null 207 | 208 | try { 209 | await handleError(example.response) 210 | } catch (e: any) { 211 | error = e 212 | } 213 | 214 | expect(error).not.toBeNull() 215 | expect(error.name).toEqual(example.ename) 216 | expect(error.code).toEqual(example.code) 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /test/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { AuthInvalidJwtError } from '../src' 2 | import { 3 | decodeJWT, 4 | getAlgorithm, 5 | parseParametersFromURL, 6 | parseResponseAPIVersion, 7 | } from '../src/lib/helpers' 8 | 9 | describe('parseParametersFromURL', () => { 10 | it('should parse parameters from a URL with query params only', () => { 11 | const url = new URL('https://supabase.com') 12 | url.searchParams.set('a', 'b') 13 | url.searchParams.set('b', 'c') 14 | 15 | const result = parseParametersFromURL(url.href) 16 | expect(result).toMatchObject({ 17 | a: 'b', 18 | b: 'c', 19 | }) 20 | }) 21 | 22 | it('should parse parameters from a URL with fragment params only', () => { 23 | const url = new URL('https://supabase.com') 24 | const fragmentParams = new URLSearchParams({ a: 'b', b: 'c' }) 25 | url.hash = fragmentParams.toString() 26 | 27 | const result = parseParametersFromURL(url.href) 28 | expect(result).toMatchObject({ 29 | a: 'b', 30 | b: 'c', 31 | }) 32 | }) 33 | 34 | it('should parse parameters from a URL with both query params and fragment params', () => { 35 | const url = new URL('https://supabase.com') 36 | url.searchParams.set('a', 'b') 37 | url.searchParams.set('b', 'c') 38 | url.searchParams.set('x', 'z') 39 | 40 | const fragmentParams = new URLSearchParams({ d: 'e', x: 'y' }) 41 | url.hash = fragmentParams.toString() 42 | 43 | const result = parseParametersFromURL(url.href) 44 | expect(result).toMatchObject({ 45 | a: 'b', 46 | b: 'c', 47 | d: 'e', 48 | x: 'z', // search params take precedence 49 | }) 50 | }) 51 | }) 52 | 53 | describe('parseResponseAPIVersion', () => { 54 | it('should parse valid dates', () => { 55 | expect( 56 | parseResponseAPIVersion({ 57 | headers: { 58 | get: () => { 59 | return '2024-01-01' 60 | }, 61 | }, 62 | } as any) 63 | ).toEqual(new Date('2024-01-01T00:00:00.0Z')) 64 | }) 65 | 66 | it('should return null on invalid dates', () => { 67 | ;['2024-01-32', '', 'notadate', 'Sat Feb 24 2024 17:59:17 GMT+0100'].forEach((example) => { 68 | expect( 69 | parseResponseAPIVersion({ 70 | headers: { 71 | get: () => { 72 | return example 73 | }, 74 | }, 75 | } as any) 76 | ).toBeNull() 77 | }) 78 | }) 79 | }) 80 | 81 | describe('decodeJWT', () => { 82 | it('should reject non-JWT strings', () => { 83 | expect(() => decodeJWT('non-jwt')).toThrowError( 84 | new AuthInvalidJwtError('Invalid JWT structure') 85 | ) 86 | expect(() => decodeJWT('aHR0.cDovL.2V4YW1wbGUuY29t')).toThrowError( 87 | new AuthInvalidJwtError('JWT not in base64url format') 88 | ) 89 | }) 90 | 91 | it('should decode JWT successfully', () => { 92 | expect( 93 | decodeJWT( 94 | 'eyJhbGciOiJFUzI1NiIsImtpZCI6ImZhM2ZmYzk5LTQ2MzUtNGIxOS1iNWMwLTZkNmE4ZDMwYzRlYiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Byb2plY3RyZWYuc3VwYWJhc2UuY28iLCJzdWIiOiI2OTAxMTJlNi04NThiLTQwYzctODBlNi05NmRiNjk3MTkyYjUiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxODM4MDk5NjcwLCJpYXQiOjE3MzgwOTk2NzAsImVtYWlsIjoiIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnt9LCJ1c2VyX21ldGFkYXRhIjp7ImNvbG9yIjoiYmx1ZSJ9LCJyb2xlIjoiIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoiYW5vbnltb3VzIiwidGltZXN0YW1wIjoxNzM4MDk5NjcwfV0sInNlc3Npb25faWQiOiI0YzZiMjg5NC00M2I0LTQ2YzQtYmQyZi0zNWM1OWVjNDRmZWYiLCJpc19hbm9ueW1vdXMiOnRydWV9.JcWCW3u4F9iFo1yV3OlxnosP7jLnOa2Q7LoPTxyFmvZc1_Kziimw8jD95EpXyTMEwKFt2dPSmWGkqdoJu6FV0Q' 95 | ) 96 | ).toMatchInlineSnapshot(` 97 | Object { 98 | "header": Object { 99 | "alg": "ES256", 100 | "kid": "fa3ffc99-4635-4b19-b5c0-6d6a8d30c4eb", 101 | "typ": "JWT", 102 | }, 103 | "payload": Object { 104 | "aal": "aal1", 105 | "amr": Array [ 106 | Object { 107 | "method": "anonymous", 108 | "timestamp": 1738099670, 109 | }, 110 | ], 111 | "app_metadata": Object {}, 112 | "aud": "authenticated", 113 | "email": "", 114 | "exp": 1838099670, 115 | "iat": 1738099670, 116 | "is_anonymous": true, 117 | "iss": "https://projectref.supabase.co", 118 | "phone": "", 119 | "role": "", 120 | "session_id": "4c6b2894-43b4-46c4-bd2f-35c59ec44fef", 121 | "sub": "690112e6-858b-40c7-80e6-96db697192b5", 122 | "user_metadata": Object { 123 | "color": "blue", 124 | }, 125 | }, 126 | "raw": Object { 127 | "header": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImZhM2ZmYzk5LTQ2MzUtNGIxOS1iNWMwLTZkNmE4ZDMwYzRlYiIsInR5cCI6IkpXVCJ9", 128 | "payload": "eyJpc3MiOiJodHRwczovL3Byb2plY3RyZWYuc3VwYWJhc2UuY28iLCJzdWIiOiI2OTAxMTJlNi04NThiLTQwYzctODBlNi05NmRiNjk3MTkyYjUiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxODM4MDk5NjcwLCJpYXQiOjE3MzgwOTk2NzAsImVtYWlsIjoiIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnt9LCJ1c2VyX21ldGFkYXRhIjp7ImNvbG9yIjoiYmx1ZSJ9LCJyb2xlIjoiIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoiYW5vbnltb3VzIiwidGltZXN0YW1wIjoxNzM4MDk5NjcwfV0sInNlc3Npb25faWQiOiI0YzZiMjg5NC00M2I0LTQ2YzQtYmQyZi0zNWM1OWVjNDRmZWYiLCJpc19hbm9ueW1vdXMiOnRydWV9", 129 | }, 130 | "signature": Uint8Array [ 131 | 37, 132 | 197, 133 | 130, 134 | 91, 135 | 123, 136 | 184, 137 | 23, 138 | 216, 139 | 133, 140 | 163, 141 | 92, 142 | 149, 143 | 220, 144 | 233, 145 | 113, 146 | 158, 147 | 139, 148 | 15, 149 | 238, 150 | 50, 151 | 231, 152 | 57, 153 | 173, 154 | 144, 155 | 236, 156 | 186, 157 | 15, 158 | 79, 159 | 28, 160 | 133, 161 | 154, 162 | 246, 163 | 92, 164 | 215, 165 | 242, 166 | 179, 167 | 138, 168 | 41, 169 | 176, 170 | 242, 171 | 48, 172 | 253, 173 | 228, 174 | 74, 175 | 87, 176 | 201, 177 | 51, 178 | 4, 179 | 192, 180 | 161, 181 | 109, 182 | 217, 183 | 211, 184 | 210, 185 | 153, 186 | 97, 187 | 164, 188 | 169, 189 | 218, 190 | 9, 191 | 187, 192 | 161, 193 | 85, 194 | 209, 195 | ], 196 | } 197 | `) 198 | }) 199 | }) 200 | 201 | describe('getAlgorithm', () => { 202 | const cases = [ 203 | { 204 | name: 'RS256', 205 | expected: { 206 | name: 'RSASSA-PKCS1-v1_5', 207 | hash: { name: 'SHA-256' }, 208 | }, 209 | }, 210 | { 211 | name: 'ES256', 212 | expected: { 213 | name: 'ECDSA', 214 | namedCurve: 'P-256', 215 | hash: { name: 'SHA-256' }, 216 | }, 217 | }, 218 | ] 219 | it('should return correct algorithm object', () => { 220 | cases.forEach((c) => { 221 | expect(getAlgorithm(c.name as any)).toEqual(c.expected) 222 | }) 223 | }) 224 | it('should throw if invalid alg claim', () => { 225 | expect(() => getAlgorithm('EdDSA' as any)).toThrowError(new Error('Invalid alg claim')) 226 | }) 227 | }) 228 | -------------------------------------------------------------------------------- /test/lib/clients.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import { GoTrueAdminApi, GoTrueClient } from '../../src/index' 3 | import { SupportedStorage } from '../../src/lib/types' 4 | 5 | export const SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 6 | 7 | export const SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 8 | export const SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 9 | export const SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT = 9996 10 | 11 | export const GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF = `http://localhost:${SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT}` 12 | export const GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON = `http://localhost:${SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT}` 13 | export const GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON = `http://localhost:${SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT}` 14 | export const GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF = `http://localhost:${SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT}` 15 | 16 | export const GOTRUE_JWT_SECRET = '37c304f8-51aa-419a-a1af-06154e63707a' 17 | 18 | const AUTH_ADMIN_JWT = jwt.sign( 19 | { 20 | sub: '1234567890', 21 | role: 'supabase_admin', 22 | }, 23 | GOTRUE_JWT_SECRET 24 | ) 25 | 26 | class MemoryStorage { 27 | private _storage: { [name: string]: string } = {} 28 | 29 | async setItem(name: string, value: string) { 30 | this._storage[name] = value 31 | } 32 | 33 | async getItem(name: string): Promise { 34 | return this._storage[name] || null 35 | } 36 | 37 | async removeItem(name: string) { 38 | delete this._storage[name] 39 | } 40 | } 41 | 42 | export const authClient = new GoTrueClient({ 43 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 44 | autoRefreshToken: false, 45 | persistSession: true, 46 | storage: new MemoryStorage(), 47 | }) 48 | 49 | export const authClientWithSession = new GoTrueClient({ 50 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 51 | autoRefreshToken: false, 52 | persistSession: true, 53 | storage: new MemoryStorage(), 54 | }) 55 | 56 | export const authClientWithAsymmetricSession = new GoTrueClient({ 57 | url: GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON, 58 | autoRefreshToken: false, 59 | persistSession: true, 60 | storage: new MemoryStorage(), 61 | }) 62 | 63 | export const authSubscriptionClient = new GoTrueClient({ 64 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 65 | autoRefreshToken: false, 66 | persistSession: true, 67 | storage: new MemoryStorage(), 68 | }) 69 | 70 | export const clientApiAutoConfirmEnabledClient = new GoTrueClient({ 71 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 72 | autoRefreshToken: false, 73 | persistSession: true, 74 | storage: new MemoryStorage(), 75 | }) 76 | 77 | export const clientApiAutoConfirmOffSignupsEnabledClient = new GoTrueClient({ 78 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, 79 | autoRefreshToken: false, 80 | persistSession: true, 81 | storage: new MemoryStorage(), 82 | }) 83 | 84 | export const clientApiAutoConfirmDisabledClient = new GoTrueClient({ 85 | url: GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, 86 | autoRefreshToken: false, 87 | persistSession: true, 88 | storage: new MemoryStorage(), 89 | }) 90 | 91 | export const pkceClient = new GoTrueClient({ 92 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 93 | autoRefreshToken: false, 94 | persistSession: true, 95 | storage: new MemoryStorage(), 96 | flowType: 'pkce', 97 | }) 98 | 99 | export const autoRefreshClient = new GoTrueClient({ 100 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 101 | autoRefreshToken: true, 102 | persistSession: true, 103 | }) 104 | 105 | export const authAdminApiAutoConfirmEnabledClient = new GoTrueAdminApi({ 106 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 107 | headers: { 108 | Authorization: `Bearer ${AUTH_ADMIN_JWT}`, 109 | }, 110 | }) 111 | 112 | export const authAdminApiAutoConfirmDisabledClient = new GoTrueAdminApi({ 113 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, 114 | headers: { 115 | Authorization: `Bearer ${AUTH_ADMIN_JWT}`, 116 | }, 117 | }) 118 | 119 | const SERVICE_ROLE_JWT = jwt.sign( 120 | { 121 | role: 'service_role', 122 | // Set issued at to 1 minute ago to fix flacky tests because of 123 | // invalid JWT: unable to parse or verify signature, Token used before issued 124 | iat: Math.floor(Date.now() / 1000) - 60 125 | }, 126 | GOTRUE_JWT_SECRET 127 | ) 128 | 129 | export const serviceRoleApiClient = new GoTrueAdminApi({ 130 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 131 | headers: { 132 | Authorization: `Bearer ${SERVICE_ROLE_JWT}`, 133 | }, 134 | }) 135 | 136 | export const serviceRoleApiClientWithSms = new GoTrueAdminApi({ 137 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, 138 | headers: { 139 | Authorization: `Bearer ${SERVICE_ROLE_JWT}`, 140 | }, 141 | }) 142 | 143 | export const serviceRoleApiClientNoSms = new GoTrueAdminApi({ 144 | url: GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, 145 | headers: { 146 | Authorization: `Bearer ${SERVICE_ROLE_JWT}`, 147 | }, 148 | }) 149 | 150 | export function getClientWithSpecificStorage(storage: SupportedStorage) { 151 | return new GoTrueClient({ 152 | url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, 153 | storageKey: 'test-specific-storage', 154 | autoRefreshToken: false, 155 | persistSession: true, 156 | storage, 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /test/lib/local-storage.test.ts: -------------------------------------------------------------------------------- 1 | import { memoryLocalStorageAdapter } from '../../src/lib/local-storage' 2 | 3 | describe('memoryLocalStorageAdapter', () => { 4 | it('sets and gets a value', () => { 5 | const adapter = memoryLocalStorageAdapter() 6 | adapter.setItem('foo', 'bar') 7 | expect(adapter.getItem('foo')).toBe('bar') 8 | }) 9 | 10 | it('returns null for unknown key', () => { 11 | const adapter = memoryLocalStorageAdapter() 12 | expect(adapter.getItem('missing')).toBeNull() 13 | }) 14 | 15 | it('removes an item', () => { 16 | const adapter = memoryLocalStorageAdapter() 17 | adapter.setItem('key', 'value') 18 | adapter.removeItem('key') 19 | expect(adapter.getItem('key')).toBeNull() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/lib/locks.test.ts: -------------------------------------------------------------------------------- 1 | import { processLock, navigatorLock } from '../../src/lib/locks' 2 | 3 | describe('navigatorLock', () => { 4 | beforeEach(() => { 5 | // Mock navigator.locks API 6 | Object.defineProperty(globalThis, 'navigator', { 7 | value: { 8 | locks: { 9 | request: jest.fn().mockImplementation((_, __, callback) => Promise.resolve(callback(null))), 10 | query: jest.fn().mockResolvedValue({ held: [] }) 11 | } 12 | }, 13 | configurable: true 14 | }) 15 | }) 16 | 17 | it('should acquire and release lock successfully', async () => { 18 | const mockLock = { name: 'test-lock' } 19 | ;(globalThis.navigator.locks.request as jest.Mock).mockImplementation((_, __, callback) => 20 | Promise.resolve(callback(mockLock)) 21 | ) 22 | 23 | const result = await navigatorLock('test', -1, async () => 'success') 24 | expect(result).toBe('success') 25 | expect(globalThis.navigator.locks.request).toHaveBeenCalled() 26 | }) 27 | 28 | it('should handle immediate acquisition failure', async () => { 29 | ;(globalThis.navigator.locks.request as jest.Mock).mockImplementation((_, __, callback) => 30 | Promise.resolve(callback(null)) 31 | ) 32 | 33 | await expect(navigatorLock('test', 0, async () => 'success')).rejects.toThrow() 34 | }) 35 | }) 36 | 37 | describe('processLock', () => { 38 | it('should serialize access correctly', async () => { 39 | const timestamps: number[] = [] 40 | const operations: Promise[] = [] 41 | 42 | let expectedDuration = 0 43 | 44 | for (let i = 0; i <= 1000; i += 1) { 45 | const acquireTimeout = Math.random() < 0.3 ? Math.ceil(10 + Math.random() * 100) : -1 46 | 47 | operations.push( 48 | (async () => { 49 | try { 50 | await processLock('name', acquireTimeout, async () => { 51 | const start = Date.now() 52 | 53 | timestamps.push(start) 54 | 55 | let diff = Date.now() - start 56 | 57 | while (diff < 10) { 58 | // setTimeout is not very precise, sometimes it actually times out a bit earlier 59 | // so this cycle ensures that it has actually taken >= 10ms 60 | await new Promise((accept) => { 61 | setTimeout(() => accept(null), Math.max(1, 10 - diff)) 62 | }) 63 | 64 | diff = Date.now() - start 65 | } 66 | 67 | expectedDuration += Date.now() - start 68 | }) 69 | } catch (e: any) { 70 | if (acquireTimeout > -1 && e && e.isAcquireTimeout) { 71 | return null 72 | } 73 | 74 | throw e 75 | } 76 | })() 77 | ) 78 | } 79 | 80 | const start = Date.now() 81 | 82 | await Promise.all(operations) 83 | 84 | const end = Date.now() 85 | 86 | expect(end - start).toBeGreaterThanOrEqual(expectedDuration) 87 | expect(Math.ceil((end - start) / timestamps.length)).toBeGreaterThanOrEqual(10) 88 | 89 | for (let i = 1; i < timestamps.length; i += 1) { 90 | expect(timestamps[i]).toBeGreaterThan(timestamps[i - 1]) 91 | } 92 | 93 | for (let i = 1; i < timestamps.length; i += 1) { 94 | expect(timestamps[i] - timestamps[i - 1]).toBeGreaterThanOrEqual(10) 95 | } 96 | }, 15_000) 97 | 98 | it('should handle timeout correctly', async () => { 99 | const operation1 = processLock('timeout-test', -1, async () => { 100 | await new Promise(resolve => setTimeout(resolve, 200)) 101 | return 'success' 102 | }) 103 | 104 | // Try to acquire same lock with timeout 105 | const operation2 = processLock('timeout-test', 100, async () => 'should timeout') 106 | 107 | await expect(operation2).rejects.toThrow() 108 | await expect(operation1).resolves.toBe('success') 109 | }) 110 | 111 | it('should handle errors in locked operation', async () => { 112 | const errorMessage = 'Test error' 113 | await expect( 114 | processLock('error-test', -1, async () => { 115 | throw new Error(errorMessage) 116 | }) 117 | ).rejects.toThrow(errorMessage) 118 | 119 | await expect( 120 | processLock('error-test', -1, async () => 'success') 121 | ).resolves.toBe('success') 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/lib/polyfills.test.ts: -------------------------------------------------------------------------------- 1 | import { polyfillGlobalThis } from '../../src/lib/polyfills' 2 | 3 | describe('polyfillGlobalThis', () => { 4 | it('should be defined as a function', () => { 5 | expect(typeof polyfillGlobalThis).toBe('function') 6 | }) 7 | }) -------------------------------------------------------------------------------- /test/lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mockUserCredentials, 3 | mockUserMetadata, 4 | mockAppMetadata, 5 | createNewUserWithEmail, 6 | } from './utils' 7 | 8 | describe('useful helper utilities when using the auth clients', () => { 9 | describe('Mocks User Credentials', () => { 10 | test('mockUserCredentials() has email', () => { 11 | const { email, password } = mockUserCredentials() 12 | 13 | expect(email).not.toBeUndefined() 14 | expect(password).not.toBeUndefined() 15 | 16 | expect(email).toMatch(/.*@.*/g) 17 | expect(password.length).toBeGreaterThan(0) 18 | }) 19 | 20 | test('mockUserCredentials() has phone', () => { 21 | const { phone, password } = mockUserCredentials() 22 | 23 | expect(phone).not.toBeUndefined() 24 | expect(password).not.toBeUndefined() 25 | 26 | expect(phone).toMatch(/\d{11}/g) 27 | expect(password.length).toBeGreaterThan(0) 28 | }) 29 | 30 | test('createNewUserWithEmail()', async () => { 31 | const email = `user+${Date.now()}@example.com` 32 | const { data } = await createNewUserWithEmail({ 33 | email, 34 | }) 35 | 36 | expect(data.user).not.toBeNull() 37 | expect(data.user?.email).not.toBeUndefined() 38 | expect(data.user?.email).toEqual(email) 39 | }) 40 | }) 41 | describe('Mocks User Metadata()', () => { 42 | test('mockUserMetadata()', async () => { 43 | const userMetadata = mockUserMetadata() 44 | 45 | expect(userMetadata).not.toBeNull() 46 | expect(userMetadata?.profile_image).not.toBeUndefined() 47 | expect(userMetadata?.profile_image).toMatch(/^https.*avatars.*(jpg|png)$/g) 48 | }) 49 | }) 50 | 51 | describe('Mocks App Metadata()', () => { 52 | test('mockAppMetadata()', async () => { 53 | const appMetadata = mockAppMetadata() 54 | 55 | expect(appMetadata).not.toBeNull() 56 | expect(appMetadata?.roles).not.toBeUndefined() 57 | expect(appMetadata?.roles.length).toBeGreaterThanOrEqual(1) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import faker from 'faker' 2 | import jwt from 'jsonwebtoken' 3 | 4 | import { serviceRoleApiClient } from './clients' 5 | 6 | import { GOTRUE_JWT_SECRET } from './clients' 7 | 8 | export const mockAccessToken = () => { 9 | return jwt.sign( 10 | { 11 | sub: '1234567890', 12 | role: 'anon_key', 13 | }, 14 | GOTRUE_JWT_SECRET 15 | ) 16 | } 17 | 18 | type Credentials = { 19 | email?: string | undefined 20 | phone?: string | undefined 21 | password?: string | undefined 22 | } 23 | 24 | export const mockUserCredentials = ( 25 | options?: Credentials 26 | ): { email: string; phone: string; password: string } => { 27 | const randNumbers = Date.now().toString() 28 | 29 | return { 30 | email: options?.email || faker.internet.email().toLowerCase(), 31 | phone: options?.phone || `1${randNumbers.substring(randNumbers.length - 12, 11)}`, 32 | password: options?.password || faker.internet.password(), 33 | } 34 | } 35 | 36 | export const mockVerificationOTP = (): string => { 37 | return Math.floor(100000 + Math.random() * 900000).toString() 38 | } 39 | 40 | export const mockUserMetadata = () => { 41 | return { 42 | profile_image: faker.image.avatar(), 43 | } 44 | } 45 | 46 | export const mockAppMetadata = () => { 47 | return { 48 | roles: ['editor', 'publisher'], 49 | } 50 | } 51 | 52 | export const createNewUserWithEmail = async ({ 53 | email, 54 | password, 55 | }: { 56 | email: string | undefined 57 | password?: string | undefined 58 | }) => { 59 | const { email: newEmail, password: newPassword } = mockUserCredentials({ 60 | email, 61 | password, 62 | }) 63 | return await serviceRoleApiClient.createUser({ 64 | email: newEmail, 65 | password: newPassword, 66 | user_metadata: {}, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "module": "CommonJS", 7 | "outDir": "dist/main", 8 | "rootDir": "src", 9 | "sourceMap": true, 10 | "target": "ES2017", 11 | 12 | "strict": true, 13 | 14 | "esModuleInterop": true, 15 | "moduleResolution": "Node", 16 | 17 | "forceConsistentCasingInFileNames": true, 18 | "stripInternal": true 19 | }, 20 | "typedocOptions": { 21 | "excludeExternals": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "dist/module" 6 | } 7 | } 8 | --------------------------------------------------------------------------------