├── .editorconfig ├── .eslintrc ├── .example.env ├── .github └── workflows │ ├── PULL_REQUEST_TEMPLATE.md │ └── build.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── docs ├── build.md ├── configuration.md ├── overview.md ├── setup.md ├── structure.md ├── tests.md └── todo.md ├── examples ├── basic │ ├── index.js │ ├── package-lock.json │ └── package.json └── detailed │ ├── .env.example │ ├── identifier │ └── index.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── session │ └── index.js │ └── user │ └── index.js ├── jest.config.js ├── openapitools.json ├── package-lock.json ├── package.json ├── src ├── config.ts ├── errors │ ├── baseError.ts │ ├── httpStatusCodes.ts │ ├── index.ts │ ├── serverError.ts │ └── validationError.ts ├── generated │ ├── .gitignore │ ├── .npmignore │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ │ ├── FILES │ │ └── VERSION │ ├── api.ts │ ├── base.ts │ ├── common.ts │ ├── configuration.ts │ ├── git_push.sh │ └── index.ts ├── helpers │ ├── assert.ts │ ├── helpers.ts │ └── index.ts ├── index.ts ├── sdk.ts ├── services │ ├── identifierService.ts │ ├── index.ts │ ├── sessionService.ts │ └── userService.ts ├── specs │ ├── backend_api_public_v2.yml │ └── common.yml └── webhook │ ├── entities │ ├── authMethodsDataRequest.ts │ ├── authMethodsDataResponse.ts │ ├── authMethodsRequest.ts │ ├── authMethodsResponse.ts │ ├── commonRequest.ts │ ├── commonResponse.ts │ ├── index.ts │ ├── passwordVerifyDataRequest.ts │ ├── passwordVerifyDataResponse.ts │ ├── passwordVerifyRequest.ts │ └── passwordVerifyResponse.ts │ └── webhook.ts ├── tests ├── config.test.ts ├── integration │ └── services │ │ ├── identifier.test.ts │ │ └── user.test.ts ├── sdk.test.ts ├── setupJest.js ├── unit │ └── session.test.ts └── utils.ts ├── tsconfig.base.json ├── tsconfig.cjs.json ├── tsconfig.eslint.json └── tsconfig.esm.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module", 7 | "project": "./tsconfig.eslint.json" 8 | }, 9 | "env": { "jest": true }, 10 | "extends": [ 11 | "airbnb-base", 12 | "airbnb-typescript/base", 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 16 | "plugin:prettier/recommended" 17 | ], 18 | "ignorePatterns": ["lib/**/*.js"], 19 | "rules": { 20 | "@typescript-eslint/restrict-template-expressions": 0, 21 | "max-classes-per-file": ["error", { "ignoreExpressions": true }], 22 | "class-methods-use-this": ["error", { "exceptMethods": ["createAnonymousUser", "#createClient"] }], 23 | "object-curly-newline": [ 24 | "error", 25 | { 26 | "ObjectExpression": { 27 | "multiline": true, 28 | "minProperties": 5 29 | }, 30 | "ObjectPattern": { 31 | "multiline": true 32 | }, 33 | "ImportDeclaration": { 34 | "multiline": true 35 | }, 36 | "ExportDeclaration": { 37 | "multiline": true 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | CORBADO_PROJECT_ID=pro-xxxxxxxxxxxxxxxxxxx 2 | CORBADO_API_SECRET=corbado1_xxxxxxxxxxxxxxxxxxxxxxxxxx 3 | CORBADO_BACKEND_API=https://backendapi.cloud.corbado.io 4 | CORBADO_FRONTEND_API=https://[project-id].frontendapi.cloud.corbado.io 5 | -------------------------------------------------------------------------------- /.github/workflows/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | **Test Configuration**: 24 | * Firmware version: 25 | * Hardware: 26 | * Toolchain: 27 | * SDK: 28 | 29 | # Checklist: 30 | 31 | - [ ] My code follows the style guidelines of this project 32 | - [ ] I have performed a self-review of my code 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | - [ ] I have added tests that prove my fix is effective or that my feature works 37 | - [ ] New and existing unit tests pass locally with my changes 38 | - [ ] Any dependent changes have been merged and published in downstream modules 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build workflow 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 2 * * *' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | 22 | - name: Cache node modules 23 | id: cache-npm 24 | uses: actions/cache@v3 25 | env: 26 | cache-name: cache-node-modules 27 | with: 28 | # npm cache files are stored in `~/.npm` on Linux/macOS 29 | path: ~/.npm 30 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-build-${{ env.cache-name }}- 33 | ${{ runner.os }}-build- 34 | ${{ runner.os }}- 35 | 36 | - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} 37 | name: List the state of node modules 38 | continue-on-error: true 39 | run: npm list 40 | 41 | - name: Install dependencies 42 | run: npm install 43 | 44 | - name: Build project 45 | run: npm run build --if-present 46 | 47 | - name: Run tests 48 | run: npm test 49 | env: 50 | CORBADO_PROJECT_ID: ${{ secrets.CORBADO_PROJECT_ID }} 51 | CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} 52 | CORBADO_FRONTEND_API: ${{ secrets.CORBADO_FRONTEND_API }} 53 | CORBADO_BACKEND_API: ${{ secrets.CORBADO_BACKEND_API }} 54 | 55 | - name: Upload coverage report to Codecov 56 | uses: codecov/codecov-action@v3 57 | env: 58 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 59 | 60 | - name: Upload jest report And Publish 61 | uses: nolleh/jest-badge-deploy-action@latest 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | .idea/ 106 | 107 | cjs/ 108 | esm/ 109 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // JS Language plugins (~required) 4 | "dbaeumer.vscode-eslint", 5 | "mgmcdermott.vscode-language-babel", 6 | "esbenp.prettier-vscode", 7 | 8 | // Highly recommended 9 | "jgclark.vscode-todo-highlight", 10 | "EditorConfig.editorconfig" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Show the path in the top window bar. 3 | "window.title": "${rootName}${separator}${activeEditorMedium}", 4 | 5 | // Formatting 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.codeActionsOnSave": ["source.fixAll.format", "source.fixAll.eslint", "source.removeUnusedImports"], 9 | 10 | // Extension settings 11 | "npm.packageManager": "npm" 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Corbado GmbH 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GEN_DIR := ./.gen 2 | TARGET_DIR := src/generated 3 | OPENAPI_SPEC_URL := https://api.corbado.com/docs/api/openapi/backend_api_public.yml 4 | OPENAPI_IMAGE := openapitools/openapi-generator-cli 5 | SOURCE_FILES := $(shell find src/ -type f -name '*.ts') 6 | 7 | .PHONY:all 8 | all: build 9 | 10 | .PHONY:build 11 | build: cjs/build esm/build 12 | 13 | .PHONY:lint 14 | lint: 15 | npx eslint --quiet 'src/**/*.ts' 'tests/**/*.ts' 16 | 17 | .PHONY:lint-fix 18 | lint-fix: fix 19 | 20 | .PHONY:fix 21 | fix: 22 | npx eslint --quiet 'src/**/*.ts' 'tests/**/*.ts' --fix 23 | 24 | .PHONY:watch 25 | watch: 26 | npx tsc --watch 27 | 28 | .PHONY:start 29 | start: build 30 | 31 | .PHONY: openapi_generate 32 | openapi_generate: 33 | npx @openapitools/openapi-generator-cli generate -i src/specs/backend_api_public_v2.yml -g typescript-axios -o src/generated --additional-properties=invokerPackage=Corbado\\Generated 34 | 35 | .PHONY:clean 36 | clean: 37 | rm -rf esm cjs $(TARGET_DIR) 38 | 39 | .PHONY: test 40 | test: 41 | @npx jest --coverage 42 | 43 | .PHONY: unittests 44 | unittests: 45 | @npx jest --coverage "/tests/unit" 46 | 47 | .PHONY: cjs/build 48 | cjs/build: $(SOURCE_FILES) 49 | npx tsc -p tsconfig.cjs.json 50 | echo '{"type": "commonjs"}' > cjs/package.json 51 | @# Creating a small file to keep track of the last build time 52 | touch cjs/build 53 | 54 | .PHONY: esm/build 55 | esm/build: $(SOURCE_FILES) 56 | npx tsc -p tsconfig.esm.json 57 | echo '{"type": "module"}' > esm/package.json 58 | @# Creating a small file to keep track of the last build time 59 | touch esm/build 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Repo Cover 2 | 3 | # Corbado Node.js SDK 4 | 5 | [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) 6 | ![Latest Stable Version](https://img.shields.io/npm/v/@corbado/node-sdk) 7 | [![Coverage Status](https://github.com/corbado/corbado-nodejs/raw/gh-pages/badges/coverage-jest%20coverage.svg?raw=true)](https://nolleh.gitcorbado/corbado-nodejs/badges/coverage-jest%20coverage.svg?raw=true) 8 | [![codecov](https://codecov.io/gh/corbado/corbado-nodejs/graph/badge.svg?token=FD4TEXN6TR)](https://codecov.io/gh/corbado/corbado-nodejs) 9 | [![documentation](https://img.shields.io/badge/documentation-Corbado_Backend_API_Reference-blue.svg)](https://apireference.cloud.corbado.io/backendapi-v2/) 10 | [![Slack](https://img.shields.io/badge/slack-join%20chat-brightgreen.svg)](https://join.slack.com/t/corbado/shared_invite/zt-1b7867yz8-V~Xr~ngmSGbt7IA~g16ZsQ) 11 | 12 | The [Corbado](https://www.corbado.com) Node SDK provides convenient access to the [Corbado Backend API](https://apireference.cloud.corbado.io/backendapi-v2/) from applications written in Node.js. 13 | 14 | [![integration-guides](https://github.com/user-attachments/assets/7859201b-a345-4b68-b336-6e2edcc6577b)](https://app.corbado.com/getting-started?search=typescript) 15 | 16 | :warning: The Corbado Node.js SDK is commonly referred to as a private client, specifically designed for usage within closed backend applications. This particular SDK should exclusively be utilized in such environments, as it is crucial to ensure that the API secret remains strictly confidential and is never shared. 17 | 18 | :rocket: [Getting started](#rocket-getting-started) | :hammer_and_wrench: [Installation](#installation) | :books: [Advanced](#books-advanced) | :speech_balloon: [Support & Feedback](#speech_balloon-support--feedback) 19 | 20 | ## :rocket: Getting started 21 | 22 | ### Requirements 23 | 24 | - Node.js 8 or higher. 25 | 26 | ## Installation 27 | 28 | Use the following command to install the Corbado Node.js SDK: 29 | 30 | ```bash 31 | npm install @corbado/node-sdk 32 | ``` 33 | 34 | ## Usage 35 | 36 | To create a Node.js SDK instance you need to provide your `Project ID` and `API secret` which can be found at the [Developer Panel](https://app.corbado.com). 37 | 38 | ### ES5: 39 | 40 | ```JavaScript 41 | const Corbado = require('@corbado/node-sdk'); 42 | 43 | const projectID = process.env.CORBADO_PROJECT_ID; 44 | const apiSecret = process.env.CORBADO_API_SECRET; 45 | const frontendAPI = process.env.CORBADO_FRONTEND_API; 46 | const backendAPI = process.env.CORBADO_BACKEND_API; 47 | 48 | const config = new Corbado.Config(projectID, apiSecret, frontendAPI, backendAPI); 49 | const sdk = new Corbado.SDK(config); 50 | ``` 51 | 52 | ### ES6: 53 | 54 | ```JavaScript 55 | import {SDK, Config} from '@corbado/node-sdk'; 56 | 57 | const projectID = process.env.CORBADO_PROJECT_ID; 58 | const apiSecret = process.env.CORBADO_API_SECRET; 59 | const frontendAPI = process.env.CORBADO_FRONTEND_API; 60 | const backendAPI = process.env.CORBADO_BACKEND_API; 61 | 62 | const config = new Config(projectID, apiSecret, frontendAPI, backendAPI); 63 | const sdk = new SDK(config); 64 | ``` 65 | 66 | ### See in action 67 | 68 | - Check [Next.js](https://github.com/corbado/passkeys-nextjs) example 69 | - Check [Express](https://github.com/corbado/passkeys-vuejs-express) example 70 | - Check [Hono](https://github.com/corbado/passkeys-react-hono) example 71 | - Check integration tests [here](tests/integration) 72 | 73 | ### Services 74 | 75 | The Corbado Node.js SDK provides the following services: 76 | 77 | - `sessions` for managing sessions ([examples](tests/unit/session.test.ts)) 78 | - `identifiers` for managing identifiers ([examples](tests/integration/services/identifier.test.ts)) 79 | - `users` for managing users ([examples](tests/integration/services/user.test.ts)) 80 | 81 | To use a specific service, such as `sessions`, invoke it as shown below: 82 | 83 | ```JavaScript 84 | corbado.sessions().validateToken(req); 85 | ``` 86 | 87 | ## :books: Advanced 88 | 89 | ### Error handling 90 | 91 | The Corbado Node.js SDK throws exceptions for all errors. The following errors are thrown: 92 | 93 | - `BaseError` for failed assertions and configuration errors (client side) 94 | - `ServerError` for server errors (server side) 95 | 96 | If the Backend API returns a HTTP status code other than 200, the Corbado Node.js SDK throws a `ServerError`. The `ServerError`class provides convenient methods to access all important data: 97 | 98 | ```javascript 99 | try { 100 | // Try to get non-existing user with ID 'usr-123456789' 101 | const user = sdk.users().get('usr-123456789'); 102 | } catch (error: ServerError) { 103 | // Show HTTP status code (404 in this case) 104 | console.log(error.getHttpStatusCode()); 105 | 106 | // Show request ID (can be used in developer panel to look up the full request 107 | // and response, see https://app.corbado.com/app/logs/requests) 108 | console.log(error.getRequestID()); 109 | 110 | // Show full request data 111 | console.log(error.getRequestData()); 112 | 113 | // Show runtime of request in seconds (server side) 114 | console.log(error.getRuntime()); 115 | 116 | // Show validation error messages (server side validation in case of HTTP 117 | // status code 400 (Bad Request)) 118 | console.log(error.getValidationMessages()); 119 | 120 | // Show full error data 121 | console.log(error.getError()); 122 | } 123 | ``` 124 | 125 | ## :speech_balloon: Support & Feedback 126 | 127 | ### Report an issue 128 | 129 | If you encounter any bugs or have suggestions, please [open an issue](https://github.com/corbado/corbado-nodejs/issues/new). 130 | 131 | ### Slack channel 132 | 133 | Join our Slack channel to discuss questions or ideas with the Corbado team and other developers. 134 | 135 | [![Slack](https://img.shields.io/badge/slack-join%20chat-brightgreen.svg)](https://join.slack.com/t/corbado/shared_invite/zt-1b7867yz8-V~Xr~ngmSGbt7IA~g16ZsQ) 136 | 137 | ### Email 138 | 139 | You can also reach out to us via email at vincent.delitz@corbado.com. 140 | 141 | ### Vulnerability reporting 142 | 143 | Please report suspected security vulnerabilities in private to security@corbado.com. Please do NOT create publicly viewable issues for suspected security vulnerabilities. 144 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build Process 2 | 3 | The Corbado Node.js SDK uses a Makefile to manage its build process. The Makefile contains several targets that perform different tasks such as building the project, running tests, and cleaning up build artifacts. 4 | 5 | ## Key Targets in the Makefile 6 | 7 | 1. **all**: This is the default target. It depends on the build target. 8 | 2. **build**: This target depends on the `cjs/build` and `esm/build` targets. It builds the project in both CommonJS and ES Module formats. 9 | 3. **cjs/build**: This target compiles the TypeScript source files to CommonJS format using the TypeScript [cjs output configuration file](../tsconfig.cjs.json). It also creates a `package.json` file in the `cjs` directory with the type set to "commonjs". 10 | 4. **esm/build**: This target compiles the TypeScript source files to ES Module format using the TypeScript [esm output configuration file](../tsconfig.esm.json). It also creates a `package.json` file in the `esm` directory with the type set to "module". 11 | 5. **test**: This target runs the tests using Jest. 12 | 6. **clean**: This target removes the `esm`, `cjs`, and `$(TARGET_DIR)` directories. 13 | 7. **lint**: This target runs ESLint on the source and test files. 14 | 8. **fix**: This target runs ESLint on the source and test files and automatically fixes any fixable problems. 15 | 9. **start**: This target depends on the build target. It's used to start the project. 16 | 10. **openapi_generate**: This target generates the API code from the OpenAPI specification file using the OpenAPI Generator. 17 | 18 | ## Building the Project 19 | 20 | To build the project, you can run the following command: 21 | 22 | ```bash 23 | npm build 24 | ``` 25 | 26 | This command runs the build target, which in turn runs the `cjs/build` and `esm/build targets`. These targets compile the TypeScript source files to CommonJS and ES Module formats, respectively. 27 | 28 | The compiled files are placed in the `cjs` and `esm` directories. These directories also contain a `package.json` file with the type set to "commonjs" or "module", respectively. 29 | 30 | This build process ensures that the project is properly compiled and ready for use in both CommonJS and ES Module environments. 31 | 32 | ## Notes 33 | 34 | Because of inherent issues between ESM and CJS, a number of important decisions were made in the project. 35 | 36 | ### Extensions in Imports 37 | 38 | All local TypeScript imports had to change from: 39 | 40 | ```javascript 41 | import User from './userService'; 42 | ``` 43 | 44 | to: 45 | 46 | ```javascript 47 | import User from './userService.js'; 48 | ``` 49 | 50 | This is because ESM requires extensions, whereas CommonJS doesn't. So building both ESM and CJS modules from our codebase requires importing files with their file extension. 51 | 52 | Note also that we're importing `userService.js` even though the file is actually `userService.ts`. This is because CJS modules will be incorrectly built with `.ts` file extensions unless we indicate `.js` as the extension ahead of time. Sadly, there is currently no TypeScript setting to automate this extension conversion. 53 | 54 | ❗ 55 | 56 | > This is why you must manually update file references in the [src/generated](../src/generated/) folder from _*filename*_ to _*filename.js*_ each time you run _npm generate-openapi_, _**before**_ building the esm and cjs modules. 57 | 58 | ### Directory imports 59 | 60 | In CJS, you can import a directory, and if an index.js exists in that directory, it will be used. ESM requires exact paths, so our statement: 61 | 62 | ```javascript 63 | import { User } from './controllers'; 64 | ``` 65 | 66 | had to become: 67 | 68 | ```javascript 69 | import { User } from './services/index.js'; 70 | ``` 71 | 72 | These notes are important, failing to follow them will result in an improperly build package which will not be fit for purpose. 73 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The Corbado Node.js SDK uses a configuration object to manage various settings. This object is created using the `Config` class, which takes 4 parameters: `projectID`, `apiSecret`, `frontendAPI` and `backendAPI`. 4 | 5 | ## Creating a Configuration Object 6 | 7 | Here is an example of how to create a configuration object: 8 | 9 | ```javascript 10 | import { Config } from '@corbado/node-sdk'; 11 | 12 | const projectID = process.env.CORBADO_PROJECT_ID; 13 | const apiSecret = process.env.CORBADO_API_SECRET; 14 | const frontendAPI = process.env.CORBADO_FRONTEND_API; 15 | const backendAPI = process.env.CORBADO_BACKEND_API; 16 | 17 | const config = new Config(projectID, apiSecret, frontendAPI, backendAPI); 18 | ``` 19 | 20 | ## Validation in Config Class 21 | 22 | The `Config` class validates the `projectID`, `apiSecret`, `frontendAPI` and `backendAPI` parameters. The `projectID` must start with 'pro-', the `apiSecret` must start with 'corbado1', both API URLs should be valid domain names and the pathname should be empty. If these conditions are not met, an error is thrown. 23 | 24 | ## Config Class Properties 25 | 26 | The `Config` class also sets several other properties: 27 | 28 | - `ShortSessionCookieName`: The name of the short session cookie. By default, this is set to `cbo_short_session`. 29 | - `CacheMaxAge`: The maximum age for the cache. By default, this is set to 60000 milliseconds (1 minute). 30 | - `JWTIssuer`: The issuer for the JWT. This is generated by appending `/.well-known/jwks` to the FrontendAPI. 31 | These properties are used throughout the SDK to configure various services. For example, the `BackendAPI`, `projectID`, and `apiSecret` are used to create an Axios client in the SDK class. 32 | 33 | ### Environment Variables 34 | 35 | Remember to set the `CORBADO_PROJECT_ID`, `CORBADO_API_SECRET`, `CORBADO_FRONTEND_API` and `CORBADO_BACKEND_API` environment variables in your project. These are used to create the `Config` object. 36 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # Project Overview 2 | 3 | ## Introduction 4 | 5 | The Corbado Node.js SDK is a comprehensive software development kit tailored for seamless integration with the Corbado Backend API. It's crafted specifically for Node.js applications, prioritizing security by ensuring that the API secret is kept confidential and used exclusively within backend environments. 6 | 7 | ## Key Features 8 | 9 | The SDK offers a robust set of features, including: 10 | 11 | - **Session Management**: Efficient handling of user sessions. 12 | - **User Management**: Provides utilities for user operations. 13 | - **Identifier Management**: Provides utilities for identifier operations. 14 | 15 | ## Installation 16 | 17 | The SDK is conveniently available on npm and can be easily installed in your Node.js project: 18 | 19 | ```bash 20 | npm install @corbado/node-sdk 21 | ``` 22 | 23 | ## Documentation 24 | 25 | For comprehensive instructions and detailed usage examples, please refer to the [README.md](../README.md) file located in the root directory of the project. 26 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup Instructions 2 | 3 | ## Prerequisites 4 | 5 | Before you begin, ensure that you have Node.js version 8 or higher installed on your machine. This is essential for the Corbado Node.js SDK to run properly. 6 | 7 | ## Installation Steps 8 | 9 | Follow these simple steps to set up the SDK: 10 | 11 | 1. **Clone the Repository** 12 | Begin by cloning the repository to your local machine using your preferred method (e.g., using Git command, GitHub desktop). 13 | 14 | 2. **Navigate to Project Directory** 15 | Once cloned, switch to the project directory: 16 | 17 | ```bash 18 | cd path/to/corbado-nodejs 19 | ``` 20 | 21 | 3. **Install Dependencies** 22 | Inside the project directory, install the necessary dependencies by running: 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | ## Key Dependencies 29 | 30 | The SDK includes several important dependencies: 31 | 32 | - `express`: A robust web application framework for Node.js. 33 | - `jose`: Provides a comprehensive collection of JWT, JWS, and JWE tools. 34 | - `typescript`: Enhances JavaScript with types for better code quality and developer experience. 35 | Refer to the package.json file for a detailed list of dependencies and their specific versions. 36 | 37 | ## Next Steps 38 | 39 | After installation, you're ready to start using the Corbado Node.js SDK in your projects. Check out the documentation and examples for guidance on how to integrate the SDK with your application. 40 | -------------------------------------------------------------------------------- /docs/structure.md: -------------------------------------------------------------------------------- 1 | # Code Structure 2 | 3 | The Corbado Node.js SDK is organized into several directories and files, each serving a specific purpose: 4 | 5 | ## Directory Structure 6 | 7 | 1. **.github**: Contains GitHub-specific files, such as the pull request template. 8 | 9 | 2. **.vscode**: Contains configuration files for Visual Studio Code. 10 | 11 | 3. **.docs**: Contains project documentation. 12 | 13 | 4. **src**: This is the main directory where the source code of the SDK resides. It contains several folders and a generated subdirectory. 14 | 15 | - **src/errors**: Contains files concerned with creating and catching errors generated elsewhere in the project. 16 | - **src/generated**: Contains the generated code for the API endpoints, generated from the OpenAPI specification file [backend_api_public.yml](../src/specs/backend_api_public.yml) using the OpenAPI Generator. Includes classes for each API endpoint and models for the request and response objects. 17 | - **src/specs**: Contains the OpenAPI specification files. 18 | - **src/helpers**: Contains helper functions and utilities that assist with validations. 19 | - **src/services**: Hosts the main services upon which the entire project is based. 20 | - **src/webhook**: Contains files related to all things request and response. 21 | 22 | - **src/config.js**: Contains the Config class, used to configure the SDK. It takes the project ID and API secret as parameters and sets several other properties. 23 | 24 | - **src/sdk.js**: Contains the SDK class, the main entry point for using the SDK. It creates an instance of the Config class and an Axios client for making API requests. 25 | 26 | 5. **tests**: This directory contains test files for the SDK, written using Jest. 27 | 28 | 6. **.env**: Used to set environment variables. Not included in the repository, but you can create your own .env file and set the `CORBADO_PROJECT_ID` and `CORBADO_API_SECRET` variables. 29 | 30 | 7. **makefile**: Contains commands for building the project, running tests, and generating the API code. 31 | 32 | 8. **package.json**: Contains metadata about the project and its dependencies. 33 | 34 | 9. **README.md**: Provides an overview of the project and instructions on how to use the SDK. 35 | 36 | ## Usage Note 37 | 38 | Remember, the SDK is designed to be used as a module in other Node.js projects. You can import the SDK class from the `src/sdk.js` file and create an instance with your project ID and API secret. 39 | -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | # Testing Framework 2 | 3 | ## Overview 4 | 5 | The Corbado Node.js SDK leverages Jest, a powerful and widely-used JavaScript testing framework, to conduct thorough unit and integration tests. These tests play a crucial role in ensuring the reliability and robustness of the SDK. 6 | 7 | ## Test Structure 8 | 9 | - **Location**: All tests are housed within the `tests` directory, neatly organized into `unit` and `integration` subdirectories for clarity. 10 | - **Utilities**: The tests utilize the `Utils` class from [tests/utils.ts](../tests/utils.ts) to generate test data and SDK instances. This class includes helpful methods like `createUser`, `createRandomTestName`, and `createRandomTestEmail`. 11 | 12 | ## Running Tests 13 | 14 | ### Prerequisites 15 | 16 | Ensure that Jest is installed, which is typically done during the initial project setup. 17 | 18 | ### Execution 19 | 20 | Run the tests with the following command: 21 | 22 | ```bash 23 | npm test 24 | ``` 25 | 26 | This command triggers the test script in the [package.json file](../package.json), which executes the Jest tests. 27 | 28 | ## Example Test Suite 29 | 30 | Consider this example of a test suite from [tests/sdk.test.ts](../tests/sdk.test.ts): 31 | 32 | ```javascript 33 | describe('SDK class', () => { 34 | let projectID; 35 | let apiSecret; 36 | let config: Configuration; 37 | let sdk: SDK; 38 | 39 | beforeEach(() => { 40 | projectID = process.env.CORBADO_PROJECT_ID; 41 | apiSecret = process.env.CORBADO_API_SECRET; 42 | 43 | if (!projectID || !apiSecret) { 44 | throw new BaseError('Env Error', 5001, 'Both projectID and apiSecret must be defined', true); 45 | } 46 | 47 | config = new Configuration(projectID, apiSecret); 48 | sdk = new SDK(config); 49 | }); 50 | 51 | it('should instantiate SDK with Configuration object', () => { 52 | expect(sdk).toBeDefined(); 53 | }); 54 | 55 | // Other tests... 56 | }); 57 | ``` 58 | 59 | This suite focuses on the SDK class, beginning with initialization in the `beforeEach` block, followed by various test cases assessing different SDK functionalities. 60 | 61 | ## Environment Variables 62 | 63 | Before running tests, ensure that `CORBADO_PROJECT_ID` and `CORBADO_API_SECRET` are set in your `.env` file, as these are crucial for creating the `Config` object in the tests. 64 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | [x] Project Overview: A brief description of the project, its purpose, and its main features. 2 | 3 | [x] Setup and Installation: Instructions on how to set up and install the project, including any dependencies. Refer to the package.json file for the list of dependencies. 4 | 5 | [x] Configuration: Explanation of the configuration options available in the project. This includes the .env file, the openapitools.json file, and the Corbado.Config object in the README.md file. 6 | 7 | [x] Code Structure: An overview of the project's code structure. This includes the main directories and files, and their purposes. 8 | 9 | [ ] API Documentation: Detailed documentation of the API endpoints, based on the backend_api_public.yml file. This should include the endpoint URLs, HTTP methods, request parameters, and response formats. 10 | 11 | [x] Generated Code: Explanation of the generated code in the src/generated directory, including how it's generated and how it's used. 12 | 13 | [x] Testing: Instructions on how to run tests, based on the test script in the package.json file and the test target in the makefile. 14 | 15 | [ ] Linting: Information about the project's linting setup, based on the lint and fix scripts in the package.json file and the corresponding targets in the makefile. 16 | 17 | [x] Build Process: Explanation of the build process, based on the build target in the makefile. 18 | 19 | [ ] Contributing: Guidelines for contributing to the project, based on the .github/workflows/PULL_REQUEST_TEMPLATE.md file. 20 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const app = express(); 4 | app.use(express.urlencoded({ extended: false })); 5 | 6 | ////////////////////////////////////////////////////////////////////////////////////////////// 7 | // Basic example which serves as basis for code snippets for integration guides // 8 | ////////////////////////////////////////////////////////////////////////////////////////////// 9 | 10 | import { Config, SDK } from '@corbado/node-sdk'; 11 | import { ValidationError } from '@corbado/node-sdk/errors'; 12 | 13 | ////////////////////////////////////////////////////////////////////////////////////////////// 14 | // Instantiate SDK // 15 | ////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | // Configuration 18 | const projectID = ''; 19 | const apiSecret = ''; 20 | const frontendAPI = ''; 21 | const backendAPI = ''; 22 | 23 | // Create SDK instance 24 | const config = new Config(projectID, apiSecret, frontendAPI, backendAPI); 25 | const sdk = new SDK(config); 26 | 27 | app.get('/', async (_, res) => { 28 | ////////////////////////////////////////////////////////////////////////////////////////////// 29 | // Protecting routes // 30 | ////////////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | // Retrieve the session-token from cookie (e.g. req.cookies.cbo_session_token) 33 | const sessionToken = ''; 34 | 35 | if (!sessionToken) { 36 | // If the session-token is empty (e.g. the cookie is not set or 37 | // expired), the user is not authenticated. e.g. redirect to login page. 38 | 39 | console.log('User not authenticated'); 40 | res.redirect('/login'); 41 | } 42 | 43 | let user; 44 | 45 | try { 46 | user = await sdk.sessions().validateToken(sessionToken); 47 | 48 | console.log(`User with ID ${user.userId} is authenticated!`); 49 | } catch (err) { 50 | if (err instanceof ValidationError) { 51 | // Handle the user not being authenticated, e.g. redirect to login page 52 | 53 | console.log(err.message, err.statusCode); 54 | res.redirect('/login'); 55 | return; 56 | } 57 | 58 | console.error(err); 59 | res.status(500).send(err.message); 60 | return; 61 | } 62 | 63 | ////////////////////////////////////////////////////////////////////////////////////////////// 64 | // Getting user data from session-token // 65 | ////////////////////////////////////////////////////////////////////////////////////////////// 66 | 67 | user = await sdk.sessions().validateToken(sessionToken); 68 | 69 | console.log('UserID', user.userId); 70 | console.log('Full Name', user.fullName); 71 | 72 | ////////////////////////////////////////////////////////////////////////////////////////////// 73 | // Getting user data from Corbado Backend API // 74 | ////////////////////////////////////////////////////////////////////////////////////////////// 75 | 76 | user = await sdk.sessions().validateToken(sessionToken); 77 | 78 | const fullUser = await sdk.users().get(user.userId); 79 | 80 | const fullName = fullUser.fullName; 81 | const userStatus = fullUser.status; 82 | 83 | console.log('User full name', fullName); 84 | console.log('User status', userStatus); 85 | 86 | // To get the email we use the IdentifierService 87 | const identifiersList = await sdk.identifiers().listByUserIdAndType(user.userId, 'email'); 88 | 89 | console.log('Email', identifiersList.identifiers[0].value); 90 | 91 | res.status(200).send('Success'); 92 | }); 93 | 94 | app.listen(8000, () => { 95 | console.log('Server running on port 8000'); 96 | }); 97 | -------------------------------------------------------------------------------- /examples/basic/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basic", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@corbado/node-sdk": "^3.0.1", 13 | "cookie-parser": "^1.4.6", 14 | "express": "^4.20.0" 15 | }, 16 | "devDependencies": { 17 | "ts-node": "^10.9.2", 18 | "typescript": "^5.3.3" 19 | } 20 | }, 21 | "node_modules/@corbado/node-sdk": { 22 | "version": "3.0.1", 23 | "resolved": "https://registry.npmjs.org/@corbado/node-sdk/-/node-sdk-3.0.1.tgz", 24 | "integrity": "sha512-HMkEPFMNx2/4cgk/2qDZ2lPeoffnM3LRwoonde0F6Omt89roIFHqEuGvV1GKO2lSJIYQLSY2dKYZD/N8exxeng==", 25 | "license": "MIT", 26 | "dependencies": { 27 | "axios": "^1.7.7", 28 | "axios-better-stacktrace": "^2.1.7", 29 | "axios-mock-adapter": "^2.0.0", 30 | "dotenv": "^16.3.1", 31 | "express": "^4.18.2", 32 | "jose": "^5.1.3", 33 | "typescript": "^5.3.4" 34 | }, 35 | "engines": { 36 | "node": ">=16.1" 37 | } 38 | }, 39 | "node_modules/@cspotcode/source-map-support": { 40 | "version": "0.8.1", 41 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 42 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 43 | "dev": true, 44 | "license": "MIT", 45 | "dependencies": { 46 | "@jridgewell/trace-mapping": "0.3.9" 47 | }, 48 | "engines": { 49 | "node": ">=12" 50 | } 51 | }, 52 | "node_modules/@jridgewell/resolve-uri": { 53 | "version": "3.1.2", 54 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 55 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 56 | "dev": true, 57 | "license": "MIT", 58 | "engines": { 59 | "node": ">=6.0.0" 60 | } 61 | }, 62 | "node_modules/@jridgewell/sourcemap-codec": { 63 | "version": "1.5.0", 64 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 65 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 66 | "dev": true, 67 | "license": "MIT" 68 | }, 69 | "node_modules/@jridgewell/trace-mapping": { 70 | "version": "0.3.9", 71 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 72 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 73 | "dev": true, 74 | "license": "MIT", 75 | "dependencies": { 76 | "@jridgewell/resolve-uri": "^3.0.3", 77 | "@jridgewell/sourcemap-codec": "^1.4.10" 78 | } 79 | }, 80 | "node_modules/@tsconfig/node10": { 81 | "version": "1.0.11", 82 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 83 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", 84 | "dev": true, 85 | "license": "MIT" 86 | }, 87 | "node_modules/@tsconfig/node12": { 88 | "version": "1.0.11", 89 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 90 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 91 | "dev": true, 92 | "license": "MIT" 93 | }, 94 | "node_modules/@tsconfig/node14": { 95 | "version": "1.0.3", 96 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 97 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 98 | "dev": true, 99 | "license": "MIT" 100 | }, 101 | "node_modules/@tsconfig/node16": { 102 | "version": "1.0.4", 103 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 104 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 105 | "dev": true, 106 | "license": "MIT" 107 | }, 108 | "node_modules/@types/node": { 109 | "version": "22.5.5", 110 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", 111 | "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", 112 | "dev": true, 113 | "license": "MIT", 114 | "peer": true, 115 | "dependencies": { 116 | "undici-types": "~6.19.2" 117 | } 118 | }, 119 | "node_modules/accepts": { 120 | "version": "1.3.8", 121 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 122 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 123 | "license": "MIT", 124 | "dependencies": { 125 | "mime-types": "~2.1.34", 126 | "negotiator": "0.6.3" 127 | }, 128 | "engines": { 129 | "node": ">= 0.6" 130 | } 131 | }, 132 | "node_modules/acorn": { 133 | "version": "8.12.1", 134 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", 135 | "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", 136 | "dev": true, 137 | "license": "MIT", 138 | "bin": { 139 | "acorn": "bin/acorn" 140 | }, 141 | "engines": { 142 | "node": ">=0.4.0" 143 | } 144 | }, 145 | "node_modules/acorn-walk": { 146 | "version": "8.3.4", 147 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 148 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 149 | "dev": true, 150 | "license": "MIT", 151 | "dependencies": { 152 | "acorn": "^8.11.0" 153 | }, 154 | "engines": { 155 | "node": ">=0.4.0" 156 | } 157 | }, 158 | "node_modules/arg": { 159 | "version": "4.1.3", 160 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 161 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 162 | "dev": true, 163 | "license": "MIT" 164 | }, 165 | "node_modules/array-flatten": { 166 | "version": "1.1.1", 167 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 168 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 169 | "license": "MIT" 170 | }, 171 | "node_modules/asynckit": { 172 | "version": "0.4.0", 173 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 174 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 175 | "license": "MIT" 176 | }, 177 | "node_modules/axios": { 178 | "version": "1.7.7", 179 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", 180 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", 181 | "license": "MIT", 182 | "dependencies": { 183 | "follow-redirects": "^1.15.6", 184 | "form-data": "^4.0.0", 185 | "proxy-from-env": "^1.1.0" 186 | } 187 | }, 188 | "node_modules/axios-better-stacktrace": { 189 | "version": "2.1.7", 190 | "resolved": "https://registry.npmjs.org/axios-better-stacktrace/-/axios-better-stacktrace-2.1.7.tgz", 191 | "integrity": "sha512-m16wNbfb7crBpENBukoBdN1G9NwqSCkuIeKjSEP2iUoFvgNUnSW1/1Ov79EkTu29xmg+TsngJcy2lfwqBzVT7g==", 192 | "license": "MIT", 193 | "peerDependencies": { 194 | "axios": "*" 195 | } 196 | }, 197 | "node_modules/axios-mock-adapter": { 198 | "version": "2.0.0", 199 | "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.0.0.tgz", 200 | "integrity": "sha512-D/K0J5Zm6KvaMTnsWrBQZWLzKN9GxUFZEa0mx2qeEHXDeTugCoplWehy8y36dj5vuSjhe1u/Dol8cZ8lzzmDew==", 201 | "license": "MIT", 202 | "dependencies": { 203 | "fast-deep-equal": "^3.1.3", 204 | "is-buffer": "^2.0.5" 205 | }, 206 | "peerDependencies": { 207 | "axios": ">= 0.17.0" 208 | } 209 | }, 210 | "node_modules/body-parser": { 211 | "version": "1.20.3", 212 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 213 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 214 | "license": "MIT", 215 | "dependencies": { 216 | "bytes": "3.1.2", 217 | "content-type": "~1.0.5", 218 | "debug": "2.6.9", 219 | "depd": "2.0.0", 220 | "destroy": "1.2.0", 221 | "http-errors": "2.0.0", 222 | "iconv-lite": "0.4.24", 223 | "on-finished": "2.4.1", 224 | "qs": "6.13.0", 225 | "raw-body": "2.5.2", 226 | "type-is": "~1.6.18", 227 | "unpipe": "1.0.0" 228 | }, 229 | "engines": { 230 | "node": ">= 0.8", 231 | "npm": "1.2.8000 || >= 1.4.16" 232 | } 233 | }, 234 | "node_modules/bytes": { 235 | "version": "3.1.2", 236 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 237 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 238 | "license": "MIT", 239 | "engines": { 240 | "node": ">= 0.8" 241 | } 242 | }, 243 | "node_modules/call-bind": { 244 | "version": "1.0.7", 245 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 246 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 247 | "license": "MIT", 248 | "dependencies": { 249 | "es-define-property": "^1.0.0", 250 | "es-errors": "^1.3.0", 251 | "function-bind": "^1.1.2", 252 | "get-intrinsic": "^1.2.4", 253 | "set-function-length": "^1.2.1" 254 | }, 255 | "engines": { 256 | "node": ">= 0.4" 257 | }, 258 | "funding": { 259 | "url": "https://github.com/sponsors/ljharb" 260 | } 261 | }, 262 | "node_modules/combined-stream": { 263 | "version": "1.0.8", 264 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 265 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 266 | "license": "MIT", 267 | "dependencies": { 268 | "delayed-stream": "~1.0.0" 269 | }, 270 | "engines": { 271 | "node": ">= 0.8" 272 | } 273 | }, 274 | "node_modules/content-disposition": { 275 | "version": "0.5.4", 276 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 277 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 278 | "license": "MIT", 279 | "dependencies": { 280 | "safe-buffer": "5.2.1" 281 | }, 282 | "engines": { 283 | "node": ">= 0.6" 284 | } 285 | }, 286 | "node_modules/content-type": { 287 | "version": "1.0.5", 288 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 289 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 290 | "license": "MIT", 291 | "engines": { 292 | "node": ">= 0.6" 293 | } 294 | }, 295 | "node_modules/cookie": { 296 | "version": "0.4.1", 297 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", 298 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", 299 | "license": "MIT", 300 | "engines": { 301 | "node": ">= 0.6" 302 | } 303 | }, 304 | "node_modules/cookie-parser": { 305 | "version": "1.4.6", 306 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", 307 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", 308 | "license": "MIT", 309 | "dependencies": { 310 | "cookie": "0.4.1", 311 | "cookie-signature": "1.0.6" 312 | }, 313 | "engines": { 314 | "node": ">= 0.8.0" 315 | } 316 | }, 317 | "node_modules/cookie-signature": { 318 | "version": "1.0.6", 319 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 320 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 321 | "license": "MIT" 322 | }, 323 | "node_modules/create-require": { 324 | "version": "1.1.1", 325 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 326 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 327 | "dev": true, 328 | "license": "MIT" 329 | }, 330 | "node_modules/debug": { 331 | "version": "2.6.9", 332 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 333 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 334 | "license": "MIT", 335 | "dependencies": { 336 | "ms": "2.0.0" 337 | } 338 | }, 339 | "node_modules/define-data-property": { 340 | "version": "1.1.4", 341 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 342 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 343 | "license": "MIT", 344 | "dependencies": { 345 | "es-define-property": "^1.0.0", 346 | "es-errors": "^1.3.0", 347 | "gopd": "^1.0.1" 348 | }, 349 | "engines": { 350 | "node": ">= 0.4" 351 | }, 352 | "funding": { 353 | "url": "https://github.com/sponsors/ljharb" 354 | } 355 | }, 356 | "node_modules/delayed-stream": { 357 | "version": "1.0.0", 358 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 359 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 360 | "license": "MIT", 361 | "engines": { 362 | "node": ">=0.4.0" 363 | } 364 | }, 365 | "node_modules/depd": { 366 | "version": "2.0.0", 367 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 368 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 369 | "license": "MIT", 370 | "engines": { 371 | "node": ">= 0.8" 372 | } 373 | }, 374 | "node_modules/destroy": { 375 | "version": "1.2.0", 376 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 377 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 378 | "license": "MIT", 379 | "engines": { 380 | "node": ">= 0.8", 381 | "npm": "1.2.8000 || >= 1.4.16" 382 | } 383 | }, 384 | "node_modules/diff": { 385 | "version": "4.0.2", 386 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 387 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 388 | "dev": true, 389 | "license": "BSD-3-Clause", 390 | "engines": { 391 | "node": ">=0.3.1" 392 | } 393 | }, 394 | "node_modules/dotenv": { 395 | "version": "16.4.5", 396 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 397 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 398 | "license": "BSD-2-Clause", 399 | "engines": { 400 | "node": ">=12" 401 | }, 402 | "funding": { 403 | "url": "https://dotenvx.com" 404 | } 405 | }, 406 | "node_modules/ee-first": { 407 | "version": "1.1.1", 408 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 409 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 410 | "license": "MIT" 411 | }, 412 | "node_modules/encodeurl": { 413 | "version": "2.0.0", 414 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 415 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 416 | "license": "MIT", 417 | "engines": { 418 | "node": ">= 0.8" 419 | } 420 | }, 421 | "node_modules/es-define-property": { 422 | "version": "1.0.0", 423 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 424 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 425 | "license": "MIT", 426 | "dependencies": { 427 | "get-intrinsic": "^1.2.4" 428 | }, 429 | "engines": { 430 | "node": ">= 0.4" 431 | } 432 | }, 433 | "node_modules/es-errors": { 434 | "version": "1.3.0", 435 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 436 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 437 | "license": "MIT", 438 | "engines": { 439 | "node": ">= 0.4" 440 | } 441 | }, 442 | "node_modules/escape-html": { 443 | "version": "1.0.3", 444 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 445 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 446 | "license": "MIT" 447 | }, 448 | "node_modules/etag": { 449 | "version": "1.8.1", 450 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 451 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 452 | "license": "MIT", 453 | "engines": { 454 | "node": ">= 0.6" 455 | } 456 | }, 457 | "node_modules/express": { 458 | "version": "4.21.0", 459 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", 460 | "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", 461 | "license": "MIT", 462 | "dependencies": { 463 | "accepts": "~1.3.8", 464 | "array-flatten": "1.1.1", 465 | "body-parser": "1.20.3", 466 | "content-disposition": "0.5.4", 467 | "content-type": "~1.0.4", 468 | "cookie": "0.6.0", 469 | "cookie-signature": "1.0.6", 470 | "debug": "2.6.9", 471 | "depd": "2.0.0", 472 | "encodeurl": "~2.0.0", 473 | "escape-html": "~1.0.3", 474 | "etag": "~1.8.1", 475 | "finalhandler": "1.3.1", 476 | "fresh": "0.5.2", 477 | "http-errors": "2.0.0", 478 | "merge-descriptors": "1.0.3", 479 | "methods": "~1.1.2", 480 | "on-finished": "2.4.1", 481 | "parseurl": "~1.3.3", 482 | "path-to-regexp": "0.1.10", 483 | "proxy-addr": "~2.0.7", 484 | "qs": "6.13.0", 485 | "range-parser": "~1.2.1", 486 | "safe-buffer": "5.2.1", 487 | "send": "0.19.0", 488 | "serve-static": "1.16.2", 489 | "setprototypeof": "1.2.0", 490 | "statuses": "2.0.1", 491 | "type-is": "~1.6.18", 492 | "utils-merge": "1.0.1", 493 | "vary": "~1.1.2" 494 | }, 495 | "engines": { 496 | "node": ">= 0.10.0" 497 | } 498 | }, 499 | "node_modules/express/node_modules/cookie": { 500 | "version": "0.6.0", 501 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 502 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 503 | "license": "MIT", 504 | "engines": { 505 | "node": ">= 0.6" 506 | } 507 | }, 508 | "node_modules/express/node_modules/path-to-regexp": { 509 | "version": "0.1.10", 510 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", 511 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", 512 | "license": "MIT" 513 | }, 514 | "node_modules/fast-deep-equal": { 515 | "version": "3.1.3", 516 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 517 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 518 | "license": "MIT" 519 | }, 520 | "node_modules/finalhandler": { 521 | "version": "1.3.1", 522 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 523 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 524 | "license": "MIT", 525 | "dependencies": { 526 | "debug": "2.6.9", 527 | "encodeurl": "~2.0.0", 528 | "escape-html": "~1.0.3", 529 | "on-finished": "2.4.1", 530 | "parseurl": "~1.3.3", 531 | "statuses": "2.0.1", 532 | "unpipe": "~1.0.0" 533 | }, 534 | "engines": { 535 | "node": ">= 0.8" 536 | } 537 | }, 538 | "node_modules/follow-redirects": { 539 | "version": "1.15.9", 540 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 541 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 542 | "funding": [ 543 | { 544 | "type": "individual", 545 | "url": "https://github.com/sponsors/RubenVerborgh" 546 | } 547 | ], 548 | "license": "MIT", 549 | "engines": { 550 | "node": ">=4.0" 551 | }, 552 | "peerDependenciesMeta": { 553 | "debug": { 554 | "optional": true 555 | } 556 | } 557 | }, 558 | "node_modules/form-data": { 559 | "version": "4.0.0", 560 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 561 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 562 | "license": "MIT", 563 | "dependencies": { 564 | "asynckit": "^0.4.0", 565 | "combined-stream": "^1.0.8", 566 | "mime-types": "^2.1.12" 567 | }, 568 | "engines": { 569 | "node": ">= 6" 570 | } 571 | }, 572 | "node_modules/forwarded": { 573 | "version": "0.2.0", 574 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 575 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 576 | "license": "MIT", 577 | "engines": { 578 | "node": ">= 0.6" 579 | } 580 | }, 581 | "node_modules/fresh": { 582 | "version": "0.5.2", 583 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 584 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 585 | "license": "MIT", 586 | "engines": { 587 | "node": ">= 0.6" 588 | } 589 | }, 590 | "node_modules/function-bind": { 591 | "version": "1.1.2", 592 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 593 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 594 | "license": "MIT", 595 | "funding": { 596 | "url": "https://github.com/sponsors/ljharb" 597 | } 598 | }, 599 | "node_modules/get-intrinsic": { 600 | "version": "1.2.4", 601 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 602 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 603 | "license": "MIT", 604 | "dependencies": { 605 | "es-errors": "^1.3.0", 606 | "function-bind": "^1.1.2", 607 | "has-proto": "^1.0.1", 608 | "has-symbols": "^1.0.3", 609 | "hasown": "^2.0.0" 610 | }, 611 | "engines": { 612 | "node": ">= 0.4" 613 | }, 614 | "funding": { 615 | "url": "https://github.com/sponsors/ljharb" 616 | } 617 | }, 618 | "node_modules/gopd": { 619 | "version": "1.0.1", 620 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 621 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 622 | "license": "MIT", 623 | "dependencies": { 624 | "get-intrinsic": "^1.1.3" 625 | }, 626 | "funding": { 627 | "url": "https://github.com/sponsors/ljharb" 628 | } 629 | }, 630 | "node_modules/has-property-descriptors": { 631 | "version": "1.0.2", 632 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 633 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 634 | "license": "MIT", 635 | "dependencies": { 636 | "es-define-property": "^1.0.0" 637 | }, 638 | "funding": { 639 | "url": "https://github.com/sponsors/ljharb" 640 | } 641 | }, 642 | "node_modules/has-proto": { 643 | "version": "1.0.3", 644 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 645 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 646 | "license": "MIT", 647 | "engines": { 648 | "node": ">= 0.4" 649 | }, 650 | "funding": { 651 | "url": "https://github.com/sponsors/ljharb" 652 | } 653 | }, 654 | "node_modules/has-symbols": { 655 | "version": "1.0.3", 656 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 657 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 658 | "license": "MIT", 659 | "engines": { 660 | "node": ">= 0.4" 661 | }, 662 | "funding": { 663 | "url": "https://github.com/sponsors/ljharb" 664 | } 665 | }, 666 | "node_modules/hasown": { 667 | "version": "2.0.2", 668 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 669 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 670 | "license": "MIT", 671 | "dependencies": { 672 | "function-bind": "^1.1.2" 673 | }, 674 | "engines": { 675 | "node": ">= 0.4" 676 | } 677 | }, 678 | "node_modules/http-errors": { 679 | "version": "2.0.0", 680 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 681 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 682 | "license": "MIT", 683 | "dependencies": { 684 | "depd": "2.0.0", 685 | "inherits": "2.0.4", 686 | "setprototypeof": "1.2.0", 687 | "statuses": "2.0.1", 688 | "toidentifier": "1.0.1" 689 | }, 690 | "engines": { 691 | "node": ">= 0.8" 692 | } 693 | }, 694 | "node_modules/iconv-lite": { 695 | "version": "0.4.24", 696 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 697 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 698 | "license": "MIT", 699 | "dependencies": { 700 | "safer-buffer": ">= 2.1.2 < 3" 701 | }, 702 | "engines": { 703 | "node": ">=0.10.0" 704 | } 705 | }, 706 | "node_modules/inherits": { 707 | "version": "2.0.4", 708 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 709 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 710 | "license": "ISC" 711 | }, 712 | "node_modules/ipaddr.js": { 713 | "version": "1.9.1", 714 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 715 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 716 | "license": "MIT", 717 | "engines": { 718 | "node": ">= 0.10" 719 | } 720 | }, 721 | "node_modules/is-buffer": { 722 | "version": "2.0.5", 723 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", 724 | "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", 725 | "funding": [ 726 | { 727 | "type": "github", 728 | "url": "https://github.com/sponsors/feross" 729 | }, 730 | { 731 | "type": "patreon", 732 | "url": "https://www.patreon.com/feross" 733 | }, 734 | { 735 | "type": "consulting", 736 | "url": "https://feross.org/support" 737 | } 738 | ], 739 | "license": "MIT", 740 | "engines": { 741 | "node": ">=4" 742 | } 743 | }, 744 | "node_modules/jose": { 745 | "version": "5.9.2", 746 | "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz", 747 | "integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==", 748 | "license": "MIT", 749 | "funding": { 750 | "url": "https://github.com/sponsors/panva" 751 | } 752 | }, 753 | "node_modules/make-error": { 754 | "version": "1.3.6", 755 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 756 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 757 | "dev": true, 758 | "license": "ISC" 759 | }, 760 | "node_modules/media-typer": { 761 | "version": "0.3.0", 762 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 763 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 764 | "license": "MIT", 765 | "engines": { 766 | "node": ">= 0.6" 767 | } 768 | }, 769 | "node_modules/merge-descriptors": { 770 | "version": "1.0.3", 771 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 772 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 773 | "license": "MIT", 774 | "funding": { 775 | "url": "https://github.com/sponsors/sindresorhus" 776 | } 777 | }, 778 | "node_modules/methods": { 779 | "version": "1.1.2", 780 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 781 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 782 | "license": "MIT", 783 | "engines": { 784 | "node": ">= 0.6" 785 | } 786 | }, 787 | "node_modules/mime": { 788 | "version": "1.6.0", 789 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 790 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 791 | "license": "MIT", 792 | "bin": { 793 | "mime": "cli.js" 794 | }, 795 | "engines": { 796 | "node": ">=4" 797 | } 798 | }, 799 | "node_modules/mime-db": { 800 | "version": "1.52.0", 801 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 802 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 803 | "license": "MIT", 804 | "engines": { 805 | "node": ">= 0.6" 806 | } 807 | }, 808 | "node_modules/mime-types": { 809 | "version": "2.1.35", 810 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 811 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 812 | "license": "MIT", 813 | "dependencies": { 814 | "mime-db": "1.52.0" 815 | }, 816 | "engines": { 817 | "node": ">= 0.6" 818 | } 819 | }, 820 | "node_modules/ms": { 821 | "version": "2.0.0", 822 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 823 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 824 | "license": "MIT" 825 | }, 826 | "node_modules/negotiator": { 827 | "version": "0.6.3", 828 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 829 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 830 | "license": "MIT", 831 | "engines": { 832 | "node": ">= 0.6" 833 | } 834 | }, 835 | "node_modules/object-inspect": { 836 | "version": "1.13.2", 837 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 838 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 839 | "license": "MIT", 840 | "engines": { 841 | "node": ">= 0.4" 842 | }, 843 | "funding": { 844 | "url": "https://github.com/sponsors/ljharb" 845 | } 846 | }, 847 | "node_modules/on-finished": { 848 | "version": "2.4.1", 849 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 850 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 851 | "license": "MIT", 852 | "dependencies": { 853 | "ee-first": "1.1.1" 854 | }, 855 | "engines": { 856 | "node": ">= 0.8" 857 | } 858 | }, 859 | "node_modules/parseurl": { 860 | "version": "1.3.3", 861 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 862 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 863 | "license": "MIT", 864 | "engines": { 865 | "node": ">= 0.8" 866 | } 867 | }, 868 | "node_modules/proxy-addr": { 869 | "version": "2.0.7", 870 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 871 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 872 | "license": "MIT", 873 | "dependencies": { 874 | "forwarded": "0.2.0", 875 | "ipaddr.js": "1.9.1" 876 | }, 877 | "engines": { 878 | "node": ">= 0.10" 879 | } 880 | }, 881 | "node_modules/proxy-from-env": { 882 | "version": "1.1.0", 883 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 884 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 885 | "license": "MIT" 886 | }, 887 | "node_modules/qs": { 888 | "version": "6.13.0", 889 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 890 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 891 | "license": "BSD-3-Clause", 892 | "dependencies": { 893 | "side-channel": "^1.0.6" 894 | }, 895 | "engines": { 896 | "node": ">=0.6" 897 | }, 898 | "funding": { 899 | "url": "https://github.com/sponsors/ljharb" 900 | } 901 | }, 902 | "node_modules/range-parser": { 903 | "version": "1.2.1", 904 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 905 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 906 | "license": "MIT", 907 | "engines": { 908 | "node": ">= 0.6" 909 | } 910 | }, 911 | "node_modules/raw-body": { 912 | "version": "2.5.2", 913 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 914 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 915 | "license": "MIT", 916 | "dependencies": { 917 | "bytes": "3.1.2", 918 | "http-errors": "2.0.0", 919 | "iconv-lite": "0.4.24", 920 | "unpipe": "1.0.0" 921 | }, 922 | "engines": { 923 | "node": ">= 0.8" 924 | } 925 | }, 926 | "node_modules/safe-buffer": { 927 | "version": "5.2.1", 928 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 929 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 930 | "funding": [ 931 | { 932 | "type": "github", 933 | "url": "https://github.com/sponsors/feross" 934 | }, 935 | { 936 | "type": "patreon", 937 | "url": "https://www.patreon.com/feross" 938 | }, 939 | { 940 | "type": "consulting", 941 | "url": "https://feross.org/support" 942 | } 943 | ], 944 | "license": "MIT" 945 | }, 946 | "node_modules/safer-buffer": { 947 | "version": "2.1.2", 948 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 949 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 950 | "license": "MIT" 951 | }, 952 | "node_modules/send": { 953 | "version": "0.19.0", 954 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 955 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 956 | "license": "MIT", 957 | "dependencies": { 958 | "debug": "2.6.9", 959 | "depd": "2.0.0", 960 | "destroy": "1.2.0", 961 | "encodeurl": "~1.0.2", 962 | "escape-html": "~1.0.3", 963 | "etag": "~1.8.1", 964 | "fresh": "0.5.2", 965 | "http-errors": "2.0.0", 966 | "mime": "1.6.0", 967 | "ms": "2.1.3", 968 | "on-finished": "2.4.1", 969 | "range-parser": "~1.2.1", 970 | "statuses": "2.0.1" 971 | }, 972 | "engines": { 973 | "node": ">= 0.8.0" 974 | } 975 | }, 976 | "node_modules/send/node_modules/encodeurl": { 977 | "version": "1.0.2", 978 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 979 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 980 | "license": "MIT", 981 | "engines": { 982 | "node": ">= 0.8" 983 | } 984 | }, 985 | "node_modules/send/node_modules/ms": { 986 | "version": "2.1.3", 987 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 988 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 989 | "license": "MIT" 990 | }, 991 | "node_modules/serve-static": { 992 | "version": "1.16.2", 993 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 994 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 995 | "license": "MIT", 996 | "dependencies": { 997 | "encodeurl": "~2.0.0", 998 | "escape-html": "~1.0.3", 999 | "parseurl": "~1.3.3", 1000 | "send": "0.19.0" 1001 | }, 1002 | "engines": { 1003 | "node": ">= 0.8.0" 1004 | } 1005 | }, 1006 | "node_modules/set-function-length": { 1007 | "version": "1.2.2", 1008 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1009 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1010 | "license": "MIT", 1011 | "dependencies": { 1012 | "define-data-property": "^1.1.4", 1013 | "es-errors": "^1.3.0", 1014 | "function-bind": "^1.1.2", 1015 | "get-intrinsic": "^1.2.4", 1016 | "gopd": "^1.0.1", 1017 | "has-property-descriptors": "^1.0.2" 1018 | }, 1019 | "engines": { 1020 | "node": ">= 0.4" 1021 | } 1022 | }, 1023 | "node_modules/setprototypeof": { 1024 | "version": "1.2.0", 1025 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1026 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1027 | "license": "ISC" 1028 | }, 1029 | "node_modules/side-channel": { 1030 | "version": "1.0.6", 1031 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1032 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1033 | "license": "MIT", 1034 | "dependencies": { 1035 | "call-bind": "^1.0.7", 1036 | "es-errors": "^1.3.0", 1037 | "get-intrinsic": "^1.2.4", 1038 | "object-inspect": "^1.13.1" 1039 | }, 1040 | "engines": { 1041 | "node": ">= 0.4" 1042 | }, 1043 | "funding": { 1044 | "url": "https://github.com/sponsors/ljharb" 1045 | } 1046 | }, 1047 | "node_modules/statuses": { 1048 | "version": "2.0.1", 1049 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1050 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1051 | "license": "MIT", 1052 | "engines": { 1053 | "node": ">= 0.8" 1054 | } 1055 | }, 1056 | "node_modules/toidentifier": { 1057 | "version": "1.0.1", 1058 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1059 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1060 | "license": "MIT", 1061 | "engines": { 1062 | "node": ">=0.6" 1063 | } 1064 | }, 1065 | "node_modules/ts-node": { 1066 | "version": "10.9.2", 1067 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1068 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1069 | "dev": true, 1070 | "license": "MIT", 1071 | "dependencies": { 1072 | "@cspotcode/source-map-support": "^0.8.0", 1073 | "@tsconfig/node10": "^1.0.7", 1074 | "@tsconfig/node12": "^1.0.7", 1075 | "@tsconfig/node14": "^1.0.0", 1076 | "@tsconfig/node16": "^1.0.2", 1077 | "acorn": "^8.4.1", 1078 | "acorn-walk": "^8.1.1", 1079 | "arg": "^4.1.0", 1080 | "create-require": "^1.1.0", 1081 | "diff": "^4.0.1", 1082 | "make-error": "^1.1.1", 1083 | "v8-compile-cache-lib": "^3.0.1", 1084 | "yn": "3.1.1" 1085 | }, 1086 | "bin": { 1087 | "ts-node": "dist/bin.js", 1088 | "ts-node-cwd": "dist/bin-cwd.js", 1089 | "ts-node-esm": "dist/bin-esm.js", 1090 | "ts-node-script": "dist/bin-script.js", 1091 | "ts-node-transpile-only": "dist/bin-transpile.js", 1092 | "ts-script": "dist/bin-script-deprecated.js" 1093 | }, 1094 | "peerDependencies": { 1095 | "@swc/core": ">=1.2.50", 1096 | "@swc/wasm": ">=1.2.50", 1097 | "@types/node": "*", 1098 | "typescript": ">=2.7" 1099 | }, 1100 | "peerDependenciesMeta": { 1101 | "@swc/core": { 1102 | "optional": true 1103 | }, 1104 | "@swc/wasm": { 1105 | "optional": true 1106 | } 1107 | } 1108 | }, 1109 | "node_modules/type-is": { 1110 | "version": "1.6.18", 1111 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1112 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1113 | "license": "MIT", 1114 | "dependencies": { 1115 | "media-typer": "0.3.0", 1116 | "mime-types": "~2.1.24" 1117 | }, 1118 | "engines": { 1119 | "node": ">= 0.6" 1120 | } 1121 | }, 1122 | "node_modules/typescript": { 1123 | "version": "5.6.2", 1124 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", 1125 | "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", 1126 | "license": "Apache-2.0", 1127 | "bin": { 1128 | "tsc": "bin/tsc", 1129 | "tsserver": "bin/tsserver" 1130 | }, 1131 | "engines": { 1132 | "node": ">=14.17" 1133 | } 1134 | }, 1135 | "node_modules/undici-types": { 1136 | "version": "6.19.8", 1137 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1138 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 1139 | "dev": true, 1140 | "license": "MIT", 1141 | "peer": true 1142 | }, 1143 | "node_modules/unpipe": { 1144 | "version": "1.0.0", 1145 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1146 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1147 | "license": "MIT", 1148 | "engines": { 1149 | "node": ">= 0.8" 1150 | } 1151 | }, 1152 | "node_modules/utils-merge": { 1153 | "version": "1.0.1", 1154 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1155 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1156 | "license": "MIT", 1157 | "engines": { 1158 | "node": ">= 0.4.0" 1159 | } 1160 | }, 1161 | "node_modules/v8-compile-cache-lib": { 1162 | "version": "3.0.1", 1163 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 1164 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 1165 | "dev": true, 1166 | "license": "MIT" 1167 | }, 1168 | "node_modules/vary": { 1169 | "version": "1.1.2", 1170 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1171 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1172 | "license": "MIT", 1173 | "engines": { 1174 | "node": ">= 0.8" 1175 | } 1176 | }, 1177 | "node_modules/yn": { 1178 | "version": "3.1.1", 1179 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1180 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1181 | "dev": true, 1182 | "license": "MIT", 1183 | "engines": { 1184 | "node": ">=6" 1185 | } 1186 | } 1187 | } 1188 | } 1189 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "type": "module", 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@corbado/node-sdk": "^3.0.1", 14 | "cookie-parser": "^1.4.6", 15 | "express": "^4.20.0" 16 | }, 17 | "devDependencies": { 18 | "ts-node": "^10.9.2", 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/detailed/.env.example: -------------------------------------------------------------------------------- 1 | CORBADO_PROJECT_ID=pro-xxxxxxxxxxxxxxx 2 | CORBADO_PROJECT_API_SECRET=corbado1_secret 3 | CORBADO_FRONTEND_API=https://[projectId].frontendapi.corbado.io 4 | CORBADO_BACKEND_API=https://backendapi.cloud.corbado.io 5 | -------------------------------------------------------------------------------- /examples/detailed/identifier/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Config, SDK } from '@corbado/node-sdk'; 3 | 4 | const router = express.Router(); 5 | const config = new Config( 6 | process.env.CORBADO_PROJECT_ID, 7 | process.env.CORBADO_PROJECT_API_SECRET, 8 | process.env.CORBADO_FRONTEND_API, 9 | process.env.CORBADO_BACKEND_API, 10 | ); 11 | const sdk = new SDK(config); 12 | 13 | // Route to create a new identifier for a user 14 | router.post('/create/:userId', async (req, res) => { 15 | try { 16 | const { userId } = req.params; 17 | const identifier = await sdk.identifiers().create(userId, req.body); // Assume req.body contains the IdentifierCreateReq data 18 | res.status(201).json(identifier); 19 | } catch (error) { 20 | console.error('Error creating identifier:', error); 21 | res.status(500).send({ message: error.message }); 22 | } 23 | }); 24 | 25 | // Route to delete an identifier by user ID and identifier ID 26 | router.delete('/delete/:userId/:identifierId', async (req, res) => { 27 | try { 28 | const { userId, identifierId } = req.params; 29 | const result = await sdk.identifiers().delete(userId, identifierId); 30 | res.status(200).json(result); 31 | } catch (error) { 32 | console.error('Error deleting identifier:', error); 33 | res.status(500).send({ message: error.message }); 34 | } 35 | }); 36 | 37 | // Route to list identifiers with optional filters, sorting, pagination 38 | router.get('/list', async (req, res) => { 39 | try { 40 | const { filter, sort, page, pageSize } = req.query; 41 | const identifiers = await sdk.identifiers().list(filter, sort, page, pageSize); 42 | res.status(200).json(identifiers); 43 | } catch (error) { 44 | console.error('Error listing identifiers:', error); 45 | res.status(500).send({ message: error.message }); 46 | } 47 | }); 48 | 49 | // Route to list identifiers by value and type 50 | router.get('/listByValueAndType', async (req, res) => { 51 | try { 52 | const { value, type, sort, page, pageSize } = req.query; 53 | if (!value || !type) { 54 | return res.status(400).send({ message: '"value" and "type" are required' }); 55 | } 56 | const identifiers = await sdk.identifiers().listByValueAndType(value, type, sort, page, pageSize); 57 | res.status(200).json(identifiers); 58 | } catch (error) { 59 | console.error('Error listing identifiers by value and type:', error); 60 | res.status(500).send({ message: error.message }); 61 | } 62 | }); 63 | 64 | // Route to list identifiers by user ID 65 | router.get('/listByUserId/:userId', async (req, res) => { 66 | try { 67 | const { userId } = req.params; 68 | const { sort, page, pageSize } = req.query; 69 | const identifiers = await sdk.identifiers().listByUserId(userId, sort, page, pageSize); 70 | res.status(200).json(identifiers); 71 | } catch (error) { 72 | console.error('Error listing identifiers by user ID:', error); 73 | res.status(500).send({ message: error.message }); 74 | } 75 | }); 76 | 77 | // Route to update the status of an identifier by user ID and identifier ID 78 | router.put('/updateStatus/:userId/:identifierId', async (req, res) => { 79 | try { 80 | const { userId, identifierId } = req.params; 81 | const { status } = req.body; 82 | if (!status) { 83 | return res.status(400).send({ message: '"status" is required' }); 84 | } 85 | const identifier = await sdk.identifiers().updateStatus(userId, identifierId, status); 86 | res.status(200).json(identifier); 87 | } catch (error) { 88 | console.error('Error updating identifier status:', error); 89 | res.status(500).send({ message: error.message }); 90 | } 91 | }); 92 | 93 | export default router; 94 | -------------------------------------------------------------------------------- /examples/detailed/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cookieParser from 'cookie-parser'; 3 | import sessionRoutes from './session/index.js'; 4 | import identifierRoutes from './identifier/index.js'; 5 | import userRoutes from './user/index.js'; 6 | 7 | const app = express(); 8 | app.use(cookieParser()); 9 | app.use(express.json()); 10 | app.use(express.urlencoded({ extended: false })); 11 | 12 | app.use('/session', sessionRoutes); 13 | app.use('/user', userRoutes); 14 | app.use('/identifier', identifierRoutes); 15 | 16 | app.listen(8000, () => { 17 | console.log('Server running on port 8000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/detailed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "example.ts", 6 | "scripts": { 7 | "start": "node --env-file=.env index.js" 8 | }, 9 | "type": "module", 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@corbado/node-sdk": "^3.0.1", 14 | "cookie-parser": "^1.4.6", 15 | "express": "^4.20.0" 16 | }, 17 | "devDependencies": { 18 | "ts-node": "^10.9.2", 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/detailed/session/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Config, SDK } from '@corbado/node-sdk'; 3 | import { ValidationError } from '@corbado/node-sdk/errors'; 4 | 5 | const router = express.Router(); 6 | const config = new Config( 7 | process.env.CORBADO_PROJECT_ID, 8 | process.env.CORBADO_PROJECT_API_SECRET, 9 | process.env.CORBADO_FRONTEND_API, 10 | process.env.CORBADO_BACKEND_API, 11 | ); 12 | const sdk = new SDK(config); 13 | 14 | router.get('/', async (_, res) => { 15 | res.send('Hello world!'); 16 | }); 17 | 18 | router.get('/setCookie', async (req, res) => { 19 | // You'll have to supply your own session-token here. 20 | // Bear in mind that the session-token is only valid for 15 minutes. 21 | // If you change the cookie name via config.setSessionTokenCookieName, you'll 22 | // have to update the cookie name here asa well. 23 | const { sessionToken } = await req.query; 24 | 25 | res.cookie('cbo_session_token', sessionToken, { maxAge: 900000, httpOnly: true }); 26 | res.send('Cookie set!'); 27 | }); 28 | 29 | router.get('/logged-in', async (req, res) => { 30 | try { 31 | const sessionToken = await req.cookies.cbo_session_token; 32 | const user = await sdk.sessions().validateToken(sessionToken); 33 | 34 | res.write(`User ID: ${user.userId}\n`); 35 | res.write(`User full name: ${user.fullName}\n`); 36 | res.end(); 37 | } catch (err) { 38 | if (err instanceof ValidationError) { 39 | res.status(401).send('Unauthorized'); 40 | return; 41 | } 42 | 43 | console.error(err); 44 | res.status(500).send(err.message); 45 | } 46 | }); 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /examples/detailed/user/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Config, SDK } from '@corbado/node-sdk'; 3 | 4 | const router = express.Router(); 5 | const config = new Config( 6 | process.env.CORBADO_PROJECT_ID, 7 | process.env.CORBADO_PROJECT_API_SECRET, 8 | process.env.CORBADO_FRONTEND_API, 9 | process.env.CORBADO_BACKEND_API, 10 | ); 11 | const sdk = new SDK(config); 12 | 13 | const userService = sdk.users(); 14 | 15 | // Route to create a new user 16 | router.post('/create', async (req, res) => { 17 | try { 18 | const user = await userService.create(req.body); // Assume req.body contains the UserCreateReq data 19 | res.status(201).json(user); 20 | } catch (error) { 21 | console.error('Error creating user:', error); 22 | res.status(500).send({ message: error.message }); 23 | } 24 | }); 25 | 26 | // Route to create a new active user by full name 27 | router.post('/createActiveByName', async (req, res) => { 28 | try { 29 | const { fullName } = req.body; 30 | if (!fullName) { 31 | return res.status(400).send({ message: '"fullName" is required' }); 32 | } 33 | const user = await userService.createActiveByName(fullName); 34 | res.status(201).json(user); 35 | } catch (error) { 36 | console.error('Error creating active user:', error); 37 | res.status(500).send({ message: error.message }); 38 | } 39 | }); 40 | 41 | // Route to delete a user by ID 42 | router.delete('/delete/:id', async (req, res) => { 43 | try { 44 | const { id } = req.params; 45 | const result = await userService.delete(id); 46 | res.status(200).json(result); 47 | } catch (error) { 48 | console.error('Error deleting user:', error); 49 | res.status(500).send({ message: error.message }); 50 | } 51 | }); 52 | 53 | // Route to get a user by ID 54 | router.get('/get/:id', async (req, res) => { 55 | try { 56 | const { id } = req.params; 57 | const user = await userService.get(id); 58 | res.status(200).json(user); 59 | } catch (error) { 60 | console.error('Error retrieving user:', error); 61 | res.status(500).send({ message: error.message }); 62 | } 63 | }); 64 | 65 | export default router; 66 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' }, 6 | extensionsToTreatAsEsm: ['.ts'], 7 | transform: { '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.esm.json' }] }, 8 | setupFiles: ['./tests/setupJest.js'], 9 | collectCoverage: true, 10 | coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover'], 11 | coverageDirectory: './coverage', 12 | }; 13 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@corbado/node-sdk", 3 | "version": "3.0.4", 4 | "description": "This Node.js SDK eases the integration of Corbado's passkey-first authentication solution.", 5 | "keywords": [ 6 | "passkeys", 7 | "authentication", 8 | "webhooks", 9 | "fido2" 10 | ], 11 | "homepage": "https://github.com/corbado/corbado-nodejs#readme", 12 | "bugs": { 13 | "url": "https://github.com/corbado/corbado-nodejs/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/corbado/corbado-nodejs.git" 18 | }, 19 | "license": "MIT", 20 | "author": "Corbado", 21 | "type": "module", 22 | "exports": { 23 | ".": { 24 | "import": "./esm/index.js", 25 | "require": "./cjs/index.js" 26 | }, 27 | "./errors": { 28 | "import": "./esm/errors/index.js", 29 | "require": "./cjs/errors/index.js" 30 | } 31 | }, 32 | "main": "cjs/index.js", 33 | "module": "src/index.js", 34 | "scripts": { 35 | "generate-openapi": "make openapi_generate", 36 | "prepublishOnly": "make build", 37 | "test": "make test", 38 | "unittests": "make unittests", 39 | "lint": "make lint", 40 | "fix": "make fix", 41 | "tsc": "tsc", 42 | "start": "make start", 43 | "build": "make build" 44 | }, 45 | "dependencies": { 46 | "axios": "^1.7.7", 47 | "axios-better-stacktrace": "^2.1.7", 48 | "axios-mock-adapter": "^2.0.0", 49 | "dotenv": "^16.3.1", 50 | "express": "^4.18.2", 51 | "jose": "^5.1.3", 52 | "typescript": "^5.3.4" 53 | }, 54 | "devDependencies": { 55 | "@openapitools/openapi-generator-cli": "^2.13.5", 56 | "@types/dotenv": "^8.2.0", 57 | "@types/express": "^4.17.21", 58 | "@types/jest": "^29.5.11", 59 | "@types/node": "^20.10.4", 60 | "@types/node-fetch": "^2.6.10", 61 | "@types/sinon": "^17.0.2", 62 | "@typescript-eslint/eslint-plugin": "^6.17.0", 63 | "@typescript-eslint/parser": "^6.17.0", 64 | "eslint": "^8.56.0", 65 | "eslint-config-airbnb-base": "^15.0.0", 66 | "eslint-config-airbnb-typescript": "^17.1.0", 67 | "eslint-config-prettier": "^9.1.0", 68 | "eslint-plugin-import": "^2.29.1", 69 | "eslint-plugin-prettier": "^5.1.2", 70 | "jest": "^29.7.0", 71 | "prettier": "^3.1.1", 72 | "ts-jest": "^29.1.1", 73 | "ts-node": "^10.9.2" 74 | }, 75 | "engines": { 76 | "node": ">=16.1" 77 | }, 78 | "files": [ 79 | "package.json", 80 | "README.md", 81 | "esm", 82 | "cjs", 83 | "LICENSE", 84 | "src" 85 | ], 86 | "mocha": { 87 | "require": [ 88 | "./test/polyfills.mjs" 89 | ], 90 | "loader": [ 91 | "ts-node/esm" 92 | ], 93 | "recursive": true, 94 | "extension": [ 95 | "ts", 96 | "js", 97 | "tsx" 98 | ] 99 | }, 100 | "nyc": { 101 | "extension": [ 102 | ".ts" 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import Assert from './helpers/assert.js'; 3 | 4 | /* eslint-disable class-methods-use-this */ 5 | export interface ConfigInterface { 6 | ProjectID: string; 7 | APISecret: string; 8 | FrontendAPI: string; 9 | BackendAPI: string; 10 | SessionTokenCookieName: string; 11 | CacheMaxAge: number; 12 | } 13 | 14 | export const DefaultClient = axios.create(); 15 | export const DefaultSessionTokenCookieName = 'cbo_session_token'; 16 | export const DefaultCacheMaxAge = 10 * 60 * 1000; // 10 * 60 * 1000 = 60000 milliseconds, which is equivalent to 10 minutes. 17 | 18 | class Config implements ConfigInterface { 19 | ProjectID: string; 20 | 21 | APISecret: string; 22 | 23 | FrontendAPI: string; 24 | 25 | BackendAPI: string; 26 | 27 | SessionTokenCookieName: string = DefaultSessionTokenCookieName; 28 | 29 | Client: AxiosInstance; 30 | 31 | CacheMaxAge: number = DefaultCacheMaxAge; 32 | 33 | constructor(projectID: string, apiSecret: string, frontendAPI: string, backendAPI: string) { 34 | this.validateProjectID(projectID); 35 | this.validateAPISecret(apiSecret); 36 | Assert.validURL(frontendAPI, 'frontendAPI'); 37 | Assert.validURL(backendAPI, 'backendAPI'); 38 | 39 | this.ProjectID = projectID; 40 | this.APISecret = apiSecret; 41 | this.Client = DefaultClient; 42 | this.FrontendAPI = frontendAPI; 43 | this.BackendAPI = backendAPI; 44 | } 45 | 46 | // @deprecated 47 | public setShortSessionCookieName(shortSessionCookieName: string): void { 48 | Assert.notEmptyString(shortSessionCookieName, 'shortSessionCookieName'); 49 | 50 | this.SessionTokenCookieName = shortSessionCookieName; 51 | } 52 | 53 | public setSessionTokenCookieName(sessionTokenName: string): void { 54 | Assert.notEmptyString(sessionTokenName, 'sessionTokenName'); 55 | 56 | this.SessionTokenCookieName = sessionTokenName; 57 | } 58 | 59 | public setHttpClient(client: AxiosInstance): void { 60 | this.Client = client; 61 | } 62 | 63 | private validateProjectID(projectID: string): void { 64 | if (!projectID || !projectID.startsWith('pro-')) { 65 | throw new Error('ProjectID must not be empty and must start with "pro-".'); 66 | } 67 | } 68 | 69 | private validateAPISecret(apiSecret: string): void { 70 | if (!apiSecret || !apiSecret.startsWith('corbado1_')) { 71 | throw new Error('APISecret must not be empty and must start with "corbado1_".'); 72 | } 73 | } 74 | } 75 | 76 | export default Config; 77 | -------------------------------------------------------------------------------- /src/errors/baseError.ts: -------------------------------------------------------------------------------- 1 | class BaseError extends Error { 2 | statusCode: number; 3 | 4 | isOperational: boolean; 5 | 6 | constructor(name: string, statusCode: number, description: string, isOperational: boolean = false) { 7 | super(description); 8 | 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | 11 | this.name = name; 12 | this.statusCode = statusCode; 13 | this.isOperational = isOperational; 14 | 15 | // We're attaching a stack trace to the error object. Certain engines like V8 finds it useful. 16 | if (typeof Error.captureStackTrace === 'function') { 17 | Error.captureStackTrace(this, this.constructor); 18 | } 19 | } 20 | } 21 | 22 | export default BaseError; 23 | -------------------------------------------------------------------------------- /src/errors/httpStatusCodes.ts: -------------------------------------------------------------------------------- 1 | type HttpStatusCode = { 2 | description: string; 3 | code: number; 4 | isOperational: boolean; 5 | }; 6 | 7 | const standardStatusCodes: Record = { 8 | OK: { description: 'Ok', code: 200, isOperational: true }, 9 | CREATED: { description: 'Created', code: 201, isOperational: true }, 10 | NO_CONTENT: { description: 'No content', code: 204, isOperational: true }, 11 | BAD_REQUEST: { description: 'Bad request', code: 400, isOperational: true }, 12 | NOT_FOUND: { description: 'Not found', code: 404, isOperational: true }, 13 | INTERNAL_SERVER_ERROR: { description: 'Internal server error', code: 500, isOperational: true }, 14 | }; 15 | 16 | const customErrorCodes: Record = { 17 | USER_ALREADY_AUTHENTICATED: { description: 'User is already authenticated', code: 1000, isOperational: false }, 18 | USER_NOT_AUTHENTICATED: { description: 'User is not authenticated', code: 1001, isOperational: false }, 19 | NULL_DATA: { description: 'Provided data is null', code: 1002, isOperational: false }, 20 | EMPTY_STRING: { description: 'Provided string is empty', code: 1003, isOperational: false }, 21 | INVALID_DATA: { description: 'Provided data is invalid', code: 1004, isOperational: false }, 22 | INVALID_KEY: { description: 'Provided key not found in set', code: 1005, isOperational: false }, 23 | STRINGIFY_FAILURE: { description: 'JSON stringify failed', code: 1006, isOperational: false }, 24 | AUTH_TOKEN_ERROR: { description: 'Unknown auth error response', code: 1007, isOperational: false }, 25 | AUTH_RSP_ERROR: { description: 'RSP error response', code: 1008, isOperational: false }, 26 | API_RESPONSE_ERROR: { description: 'Response body is noat a string', code: 1009, isOperational: false }, 27 | ISSUER_MISMATCH_ERROR: { description: 'Mismatch in issuer configuration', code: 1010, isOperational: false }, 28 | MISSING_ACTION_HEADER: { description: 'Missing action header (X-CORBADO-ACTION)', code: 1011, isOperational: false }, 29 | INVALID_ACTION_HEADER: { description: 'Missing action header (X-CORBADO-ACTION)', code: 1012, isOperational: false }, 30 | INVALID_URL: { description: 'Provided url is invalid', code: 1013, isOperational: false }, 31 | INVALID_SHORT_SESSION: { description: 'Invalid short session', code: 1014, isOperational: false }, 32 | CLAIM_VALIDATION_FAILED: { description: 'Claim validation failed', code: 1015, isOperational: false }, 33 | JWT_EXPIRED: { description: 'Token is expired', code: 1016, isOperational: false }, 34 | JWT_INVALID: { description: 'Token is invalid', code: 1017, isOperational: false }, 35 | INVALID_ISSUER: { description: 'Invalid Issuer: Issuer does not match', code: 1018, isOperational: false }, 36 | EMPTY_ISSUER: { description: 'Empty Issuer: Issuer is not defined', code: 1019, isOperational: false }, 37 | }; 38 | 39 | const httpStatusCodes = { ...standardStatusCodes, ...customErrorCodes }; 40 | 41 | export default httpStatusCodes; 42 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './baseError.js'; 2 | import ServerError, { ErrorDetails, RequestData, ServerErrorType } from './serverError.js'; 3 | import ValidationError from './validationError.js'; 4 | import httpStatusCodes from './httpStatusCodes.js'; 5 | 6 | export { BaseError, ServerError, httpStatusCodes, ErrorDetails, RequestData, ServerErrorType, ValidationError }; 7 | -------------------------------------------------------------------------------- /src/errors/serverError.ts: -------------------------------------------------------------------------------- 1 | export type ErrorDetails = { 2 | validation?: { field: string; message: string }[]; 3 | }; 4 | 5 | export type RequestData = { 6 | requestID: string; 7 | link: string; 8 | }; 9 | 10 | export type ServerErrorType = { 11 | httpStatusCode: number; 12 | message: string; 13 | requestData: RequestData; 14 | runtime: number; 15 | error: ErrorDetails; 16 | }; 17 | 18 | class ServerError extends Error { 19 | httpStatusCode: number; 20 | 21 | requestData: RequestData; 22 | 23 | runtime: number; 24 | 25 | error: ErrorDetails; 26 | 27 | constructor(httpStatusCode: number, message: string, requestData: RequestData, runtime: number, error: ErrorDetails) { 28 | super(message); 29 | 30 | this.httpStatusCode = httpStatusCode; 31 | this.requestData = requestData; 32 | this.runtime = runtime; 33 | this.error = error; 34 | 35 | this.message += ` (HTTP status code: ${httpStatusCode}, ${this.getRequestId()}, validation messages: ${this.getFlattenedValidationMessages()})`; 36 | } 37 | 38 | getHttpStatusCode() { 39 | return this.httpStatusCode; 40 | } 41 | 42 | getRequestData() { 43 | return this.requestData; 44 | } 45 | 46 | getRequestId() { 47 | return this.requestData?.requestID ?? ''; 48 | } 49 | 50 | getRuntime() { 51 | return this.runtime; 52 | } 53 | 54 | getError() { 55 | return this.error; 56 | } 57 | 58 | getValidationMessages(): string[] { 59 | const { error } = this; 60 | if (!error || !error.validation) { 61 | return []; 62 | } 63 | 64 | return error.validation.map((item) => `${item.field}: ${item.message}`); 65 | } 66 | 67 | private getFlattenedValidationMessages(): string { 68 | return this.getValidationMessages().join(', '); 69 | } 70 | } 71 | 72 | export default ServerError; 73 | -------------------------------------------------------------------------------- /src/errors/validationError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from './baseError.js'; 2 | import httpStatusCodes from './httpStatusCodes.js'; 3 | 4 | export enum ValidationErrorNames { 5 | JWTClaimValidationFailed = 'CLAIM_VALIDATION_FAILED', 6 | InvalidIssuer = 'INVALID_ISSUER', 7 | InvalidShortSession = 'INVALID_SHORT_SESSION', 8 | JWTExpired = 'JWT_EXPIRED', 9 | JWTInvalid = 'JWT_INVALID', 10 | EmptyIssuer = 'EMPTY_ISSUER', 11 | } 12 | 13 | class ValidationError extends BaseError { 14 | constructor(name: ValidationErrorNames, isOperational: boolean = false, description?: string) { 15 | super(name, httpStatusCodes[name].code, description ?? httpStatusCodes[name].description, isOperational); 16 | } 17 | } 18 | 19 | export default ValidationError; 20 | -------------------------------------------------------------------------------- /src/generated/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /src/generated/.npmignore: -------------------------------------------------------------------------------- 1 | # empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm -------------------------------------------------------------------------------- /src/generated/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /src/generated/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | .openapi-generator-ignore 4 | api.ts 5 | base.ts 6 | common.ts 7 | configuration.ts 8 | git_push.sh 9 | index.ts 10 | -------------------------------------------------------------------------------- /src/generated/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.1.0 -------------------------------------------------------------------------------- /src/generated/base.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Corbado Backend API 5 | * # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. 6 | * 7 | * The version of the OpenAPI document: 2.0.0 8 | * Contact: support@corbado.com 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | import type { Configuration } from './configuration.js'; 17 | // Some imports not used depending on template conditions 18 | // @ts-ignore 19 | import type { AxiosInstance, AxiosRequestConfig } from 'axios'; 20 | import globalAxios from 'axios'; 21 | 22 | export const BASE_PATH = "https://backendapi.corbado.io/v2".replace(/\/+$/, ""); 23 | 24 | /** 25 | * 26 | * @export 27 | */ 28 | export const COLLECTION_FORMATS = { 29 | csv: ",", 30 | ssv: " ", 31 | tsv: "\t", 32 | pipes: "|", 33 | }; 34 | 35 | /** 36 | * 37 | * @export 38 | * @interface RequestArgs 39 | */ 40 | export interface RequestArgs { 41 | url: string; 42 | options: AxiosRequestConfig; 43 | } 44 | 45 | /** 46 | * 47 | * @export 48 | * @class BaseAPI 49 | */ 50 | export class BaseAPI { 51 | protected configuration: Configuration | undefined; 52 | 53 | constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { 54 | if (configuration) { 55 | this.configuration = configuration; 56 | this.basePath = configuration.basePath ?? basePath; 57 | } 58 | } 59 | }; 60 | 61 | /** 62 | * 63 | * @export 64 | * @class RequiredError 65 | * @extends {Error} 66 | */ 67 | export class RequiredError extends Error { 68 | constructor(public field: string, msg?: string) { 69 | super(msg); 70 | this.name = "RequiredError" 71 | } 72 | } 73 | 74 | interface ServerMap { 75 | [key: string]: { 76 | url: string, 77 | description: string, 78 | }[]; 79 | } 80 | 81 | /** 82 | * 83 | * @export 84 | */ 85 | export const operationServerMap: ServerMap = { 86 | } 87 | -------------------------------------------------------------------------------- /src/generated/common.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Corbado Backend API 5 | * # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. 6 | * 7 | * The version of the OpenAPI document: 2.0.0 8 | * Contact: support@corbado.com 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | import type { Configuration } from "./configuration.js"; 17 | import type { RequestArgs } from "./base.js"; 18 | import type { AxiosInstance, AxiosResponse } from 'axios'; 19 | import { RequiredError } from "./base.js"; 20 | 21 | /** 22 | * 23 | * @export 24 | */ 25 | export const DUMMY_BASE_URL = 'https://example.com' 26 | 27 | /** 28 | * 29 | * @throws {RequiredError} 30 | * @export 31 | */ 32 | export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { 33 | if (paramValue === null || paramValue === undefined) { 34 | throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); 35 | } 36 | } 37 | 38 | /** 39 | * 40 | * @export 41 | */ 42 | export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { 43 | if (configuration && configuration.apiKey) { 44 | const localVarApiKeyValue = typeof configuration.apiKey === 'function' 45 | ? await configuration.apiKey(keyParamName) 46 | : await configuration.apiKey; 47 | object[keyParamName] = localVarApiKeyValue; 48 | } 49 | } 50 | 51 | /** 52 | * 53 | * @export 54 | */ 55 | export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { 56 | if (configuration && (configuration.username || configuration.password)) { 57 | object["auth"] = { username: configuration.username, password: configuration.password }; 58 | } 59 | } 60 | 61 | /** 62 | * 63 | * @export 64 | */ 65 | export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { 66 | if (configuration && configuration.accessToken) { 67 | const accessToken = typeof configuration.accessToken === 'function' 68 | ? await configuration.accessToken() 69 | : await configuration.accessToken; 70 | object["Authorization"] = "Bearer " + accessToken; 71 | } 72 | } 73 | 74 | /** 75 | * 76 | * @export 77 | */ 78 | export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { 79 | if (configuration && configuration.accessToken) { 80 | const localVarAccessTokenValue = typeof configuration.accessToken === 'function' 81 | ? await configuration.accessToken(name, scopes) 82 | : await configuration.accessToken; 83 | object["Authorization"] = "Bearer " + localVarAccessTokenValue; 84 | } 85 | } 86 | 87 | function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { 88 | if (parameter == null) return; 89 | if (typeof parameter === "object") { 90 | if (Array.isArray(parameter)) { 91 | (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); 92 | } 93 | else { 94 | Object.keys(parameter).forEach(currentKey => 95 | setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) 96 | ); 97 | } 98 | } 99 | else { 100 | if (urlSearchParams.has(key)) { 101 | urlSearchParams.append(key, parameter); 102 | } 103 | else { 104 | urlSearchParams.set(key, parameter); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * 111 | * @export 112 | */ 113 | export const setSearchParams = function (url: URL, ...objects: any[]) { 114 | const searchParams = new URLSearchParams(url.search); 115 | setFlattenedQueryParams(searchParams, objects); 116 | url.search = searchParams.toString(); 117 | } 118 | 119 | /** 120 | * 121 | * @export 122 | */ 123 | export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { 124 | const nonString = typeof value !== 'string'; 125 | const needsSerialization = nonString && configuration && configuration.isJsonMime 126 | ? configuration.isJsonMime(requestOptions.headers['Content-Type']) 127 | : nonString; 128 | return needsSerialization 129 | ? JSON.stringify(value !== undefined ? value : {}) 130 | : (value || ""); 131 | } 132 | 133 | /** 134 | * 135 | * @export 136 | */ 137 | export const toPathString = function (url: URL) { 138 | return url.pathname + url.search + url.hash 139 | } 140 | 141 | /** 142 | * 143 | * @export 144 | */ 145 | export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { 146 | return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 147 | const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url}; 148 | return axios.request(axiosRequestArgs); 149 | }; 150 | } 151 | -------------------------------------------------------------------------------- /src/generated/configuration.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Corbado Backend API 5 | * # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. 6 | * 7 | * The version of the OpenAPI document: 2.0.0 8 | * Contact: support@corbado.com 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | export interface ConfigurationParameters { 17 | apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); 18 | username?: string; 19 | password?: string; 20 | accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); 21 | basePath?: string; 22 | serverIndex?: number; 23 | baseOptions?: any; 24 | formDataCtor?: new () => any; 25 | } 26 | 27 | export class Configuration { 28 | /** 29 | * parameter for apiKey security 30 | * @param name security name 31 | * @memberof Configuration 32 | */ 33 | apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); 34 | /** 35 | * parameter for basic security 36 | * 37 | * @type {string} 38 | * @memberof Configuration 39 | */ 40 | username?: string; 41 | /** 42 | * parameter for basic security 43 | * 44 | * @type {string} 45 | * @memberof Configuration 46 | */ 47 | password?: string; 48 | /** 49 | * parameter for oauth2 security 50 | * @param name security name 51 | * @param scopes oauth2 scope 52 | * @memberof Configuration 53 | */ 54 | accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); 55 | /** 56 | * override base path 57 | * 58 | * @type {string} 59 | * @memberof Configuration 60 | */ 61 | basePath?: string; 62 | /** 63 | * override server index 64 | * 65 | * @type {number} 66 | * @memberof Configuration 67 | */ 68 | serverIndex?: number; 69 | /** 70 | * base options for axios calls 71 | * 72 | * @type {any} 73 | * @memberof Configuration 74 | */ 75 | baseOptions?: any; 76 | /** 77 | * The FormData constructor that will be used to create multipart form data 78 | * requests. You can inject this here so that execution environments that 79 | * do not support the FormData class can still run the generated client. 80 | * 81 | * @type {new () => FormData} 82 | */ 83 | formDataCtor?: new () => any; 84 | 85 | constructor(param: ConfigurationParameters = {}) { 86 | this.apiKey = param.apiKey; 87 | this.username = param.username; 88 | this.password = param.password; 89 | this.accessToken = param.accessToken; 90 | this.basePath = param.basePath; 91 | this.serverIndex = param.serverIndex; 92 | this.baseOptions = param.baseOptions; 93 | this.formDataCtor = param.formDataCtor; 94 | } 95 | 96 | /** 97 | * Check if the given MIME is a JSON MIME. 98 | * JSON MIME examples: 99 | * application/json 100 | * application/json; charset=UTF8 101 | * APPLICATION/JSON 102 | * application/vnd.company+json 103 | * @param mime - MIME (Multipurpose Internet Mail Extensions) 104 | * @return True if the given MIME is JSON, false otherwise. 105 | */ 106 | public isJsonMime(mime: string): boolean { 107 | const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); 108 | return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/generated/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /src/generated/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Corbado Backend API 5 | * # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. 6 | * 7 | * The version of the OpenAPI document: 2.0.0 8 | * Contact: support@corbado.com 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | export * from './api.js'; 16 | export * from './configuration.js'; 17 | -------------------------------------------------------------------------------- /src/helpers/assert.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRsp } from '../generated/index.js'; 2 | import { BaseError, httpStatusCodes } from '../errors/index.js'; 3 | 4 | const { NULL_DATA, EMPTY_STRING, INVALID_DATA, INVALID_KEY, INVALID_URL } = httpStatusCodes; 5 | 6 | export function validate( 7 | condition: boolean, 8 | errorName: string, 9 | code: number, 10 | description: string, 11 | isOperational = false, 12 | ): void { 13 | if (condition) { 14 | throw new BaseError(`Assertion Error! - ${errorName}`, code, description, isOperational); 15 | } 16 | } 17 | 18 | class Assert { 19 | public static notNull(data: unknown, errorName: string): void { 20 | validate(data == null, errorName, NULL_DATA.code, NULL_DATA.description, NULL_DATA.isOperational); 21 | } 22 | 23 | public static notEmptyString(data: string, errorName: string): void { 24 | validate(data === '', errorName, EMPTY_STRING.code, EMPTY_STRING.description, EMPTY_STRING.isOperational); 25 | } 26 | 27 | public static stringInSet(data: string, possibleValues: string[], errorName: string): void { 28 | Assert.notEmptyString(data, errorName); 29 | validate( 30 | !possibleValues.includes(data), 31 | errorName, 32 | INVALID_DATA.code, 33 | INVALID_DATA.description, 34 | INVALID_DATA.isOperational, 35 | ); 36 | } 37 | 38 | public static keysInObject(keys: string[], data: Record, errorName: string): void { 39 | keys.forEach((key) => { 40 | validate(!(key in data), errorName, INVALID_KEY.code, INVALID_KEY.description, INVALID_KEY.isOperational); 41 | }); 42 | } 43 | 44 | public static validURL(url: string, errorName: string): void { 45 | validate(!url, errorName, INVALID_URL.code, 'parse_url() returned error', INVALID_URL.isOperational); 46 | 47 | let parsedUrl: URL; 48 | try { 49 | parsedUrl = new URL(url); 50 | } catch (error) { 51 | throw new BaseError( 52 | `${errorName}URL parse failed`, 53 | INVALID_URL.code, 54 | INVALID_URL.description, 55 | INVALID_URL.isOperational, 56 | ); 57 | } 58 | 59 | validate( 60 | Boolean(parsedUrl.username), 61 | `${errorName} URL username assertion failed`, 62 | INVALID_URL.code, 63 | 'username needs to be empty', 64 | INVALID_URL.isOperational, 65 | ); 66 | 67 | validate( 68 | Boolean(parsedUrl.password), 69 | `${errorName} URL password assertion failed`, 70 | INVALID_URL.code, 71 | 'password needs to be empty', 72 | INVALID_URL.isOperational, 73 | ); 74 | 75 | validate( 76 | parsedUrl.pathname !== '/', 77 | `${errorName} URL path assertion failed`, 78 | INVALID_URL.code, 79 | 'path needs to be empty', 80 | INVALID_URL.isOperational, 81 | ); 82 | 83 | validate( 84 | Boolean(parsedUrl.search), 85 | `${errorName} URL querystring assertion failed`, 86 | INVALID_URL.code, 87 | 'querystring needs to be empty', 88 | INVALID_URL.isOperational, 89 | ); 90 | 91 | validate( 92 | Boolean(parsedUrl.hash), 93 | `${errorName} URL fragment assertion failed`, 94 | INVALID_URL.code, 95 | 'fragment needs to be empty', 96 | INVALID_URL.isOperational, 97 | ); 98 | } 99 | } 100 | 101 | export function isErrorRsp(obj: unknown): obj is ErrorRsp { 102 | return typeof obj === 'object' && obj !== null && 'error' in obj && typeof (obj as ErrorRsp).error === 'string'; 103 | } 104 | 105 | export default Assert; 106 | -------------------------------------------------------------------------------- /src/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import { BaseError, ServerError, RequestData, ServerErrorType } from '../errors/index.js'; 3 | import { GenericRsp } from '../generated/index.js'; 4 | import Assert from './assert.js'; 5 | 6 | export type ErrorWithBody = { 7 | getResponseBody?: () => string; 8 | }; 9 | 10 | class Helper { 11 | public static jsonEncode(data: unknown): string { 12 | const json = JSON.stringify(data); 13 | if (!json) { 14 | throw new BaseError('JSONEncodeError', 500, 'json_encode() failed', true); 15 | } 16 | return json; 17 | } 18 | 19 | public static jsonDecode(data: string): Record { 20 | Assert.notEmptyString(data, 'Helper.jsonDecode() data must not be an empty string'); 21 | 22 | try { 23 | return JSON.parse(data) as Record; 24 | } catch (error) { 25 | throw new BaseError('JSONDecodeError', 500, 'json_decode() failed', true); 26 | } 27 | } 28 | 29 | public static isErrorHttpStatusCode(statusCode: number): boolean { 30 | return statusCode >= 300; 31 | } 32 | 33 | public static throwServerExceptionOld(data: ServerErrorType): void { 34 | Assert.keysInObject( 35 | ['httpStatusCode', 'message', 'requestData', 'runtime'], 36 | data, 37 | 'Helper.throwServerException() "data" param must contain all required keys: httpStatusCode, message, requestData, runtime', 38 | ); 39 | 40 | const errorData = { ...data, error: data.error || {} }; 41 | throw new ServerError( 42 | errorData.httpStatusCode, 43 | 'ServerError', 44 | errorData.requestData, 45 | errorData.runtime, 46 | errorData.error, 47 | ); 48 | } 49 | 50 | public static convertToServerError(nodeError: unknown, origin: string) { 51 | if (nodeError instanceof AxiosError) { 52 | const { response } = nodeError; 53 | if (response?.data != null && 'error' in response.data) { 54 | const serverError = response.data as ServerErrorType; 55 | const status = serverError.httpStatusCode; 56 | const message = `${serverError.message} ${origin}`; 57 | const { requestData } = serverError; 58 | const { runtime } = serverError; 59 | const { error } = serverError; 60 | 61 | return new ServerError(status, message, requestData, runtime, error); 62 | } 63 | 64 | if (response) { 65 | const { status } = response; 66 | const message = JSON.stringify(response.data) || 'Internal Axios Error'; 67 | const requestData = { requestID: origin, link: '' }; 68 | const runtime = 0; 69 | const error = { validation: [] }; 70 | 71 | return new ServerError(status, message, requestData, runtime, error); 72 | } 73 | } 74 | 75 | const errorName = `${(nodeError as Error).name}`; 76 | const errorMessage = `${(nodeError as Error).message}`; 77 | 78 | const httpStatusCode = 500; 79 | const message = `Internal Server Error ${origin}`; 80 | const requestData = { requestID: '', link: '' }; 81 | const runtime = 0; 82 | const error = { validation: [{ field: errorName, message: errorMessage }] }; 83 | 84 | return new ServerError(httpStatusCode, message, requestData, runtime, error); 85 | } 86 | 87 | public static hydrateRequestData(data: Record): RequestData { 88 | Assert.keysInObject( 89 | ['requestID', 'link'], 90 | data, 91 | 'Helper.hydrateRequestData() "data" param must contain all required keys: requestID, link', 92 | ); 93 | 94 | const requestData = { requestID: data.requestID, link: data.link }; 95 | 96 | return requestData; 97 | } 98 | 99 | public static hydrateResponse(data: ServerErrorType): GenericRsp { 100 | Assert.keysInObject( 101 | ['httpStatusCode', 'message', 'requestData', 'runtime'], 102 | data, 103 | 'Helper.hydrateResponse() "data" param must contain all required keys: httpStatusCode, message, requestData, runtime', 104 | ); 105 | 106 | const requestData = Helper.hydrateRequestData(data.requestData); 107 | const { httpStatusCode, message, runtime } = data; 108 | 109 | const response: GenericRsp = { httpStatusCode, message, requestData, runtime }; 110 | 111 | return response; 112 | } 113 | } 114 | 115 | export default Helper; 116 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import Assert, { isErrorRsp } from './assert.js'; 2 | import Helper from './helpers.js'; 3 | 4 | export { Assert, Helper, isErrorRsp }; 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Config from './config.js'; 2 | import SDK from './sdk.js'; 3 | 4 | export { SDK, Config }; 5 | -------------------------------------------------------------------------------- /src/sdk.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import axios, { AxiosInstance } from 'axios'; 3 | import Config from './config.js'; 4 | import { Identifier, Session, User } from './services/index.js'; 5 | 6 | class SDK { 7 | private axiosClient: AxiosInstance; 8 | 9 | private user: User; 10 | 11 | private session: Session; 12 | 13 | private identifier: Identifier; 14 | 15 | constructor(config: Config) { 16 | this.validateEnvironment(); 17 | 18 | this.axiosClient = this.createClient(config); 19 | 20 | this.session = new Session( 21 | config.SessionTokenCookieName, 22 | config.FrontendAPI, 23 | `${config.FrontendAPI}/.well-known/jwks`, 24 | config.CacheMaxAge, 25 | config.ProjectID, 26 | ); 27 | 28 | this.user = new User(this.axiosClient); 29 | 30 | this.identifier = new Identifier(this.axiosClient); 31 | } 32 | 33 | createClient(config: Config): AxiosInstance { 34 | const instance = axios.create({ 35 | baseURL: `${config.BackendAPI}/v2`, 36 | auth: { 37 | username: config.ProjectID, 38 | password: config.APISecret, 39 | }, 40 | headers: { 41 | 'X-Corbado-SDK': JSON.stringify({ 42 | name: 'Node.js SDK', 43 | sdkVersion: process.env.npm_package_version, 44 | languageVersion: process.version, 45 | }), 46 | 'X-Corbado-ProjectID': config.ProjectID, 47 | }, 48 | }); 49 | 50 | return instance; 51 | } 52 | 53 | sessions(): Session { 54 | return this.session; 55 | } 56 | 57 | users(): User { 58 | return this.user; 59 | } 60 | 61 | identifiers(): Identifier { 62 | return this.identifier; 63 | } 64 | 65 | private validateEnvironment(): void { 66 | const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; 67 | if (isBrowser) { 68 | throw new Error('This SDK is not supported in browser environment'); 69 | } 70 | } 71 | } 72 | 73 | export default SDK; 74 | -------------------------------------------------------------------------------- /src/services/identifierService.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { 3 | IdentifierCreateReq, 4 | Identifier as IdentifierRsp, 5 | GenericRsp, 6 | IdentifierType, 7 | IdentifierUpdateReq, 8 | IdentifierStatus, 9 | IdentifiersApi, 10 | IdentifierList, 11 | } from '../generated/index.js'; 12 | import { Assert, Helper, isErrorRsp } from '../helpers/index.js'; 13 | import httpStatusCodes from '../errors/httpStatusCodes.js'; 14 | import BaseError from '../errors/baseError.js'; 15 | 16 | export interface IdentifierInterface { 17 | create(userId: string, req: IdentifierCreateReq): Promise; 18 | delete(userId: string, identifierId: string): Promise; 19 | list(filter?: string[], sort?: string, page?: number, pageSize?: number): Promise; 20 | listByValueAndType( 21 | value: string, 22 | type: IdentifierType, 23 | sort?: string, 24 | page?: number, 25 | pageSize?: number, 26 | ): Promise; 27 | listByUserId(userId: string, sort?: string, page?: number, pageSize?: number): Promise; 28 | listByUserIdAndType( 29 | userId: string, 30 | type: IdentifierType, 31 | sort?: string, 32 | page?: number, 33 | pageSize?: number, 34 | ): Promise; 35 | updateIdentifier( 36 | userId: string, 37 | identifierId: string, 38 | identifierUpdateReq: IdentifierUpdateReq, 39 | ): Promise; 40 | updateStatus(userId: string, identifierId: string, status: IdentifierStatus): Promise; 41 | } 42 | 43 | /** The user id prefix. */ 44 | const USER_ID_PREFIX = 'usr-'; 45 | 46 | class Identifier implements IdentifierInterface { 47 | private client: IdentifiersApi; 48 | 49 | constructor(axios: AxiosInstance) { 50 | Assert.notNull(axios, 'IdentifierRsp Axios instance must not be null'); 51 | this.client = new IdentifiersApi(undefined, '', axios); 52 | } 53 | 54 | async create(userId: string, req: IdentifierCreateReq): Promise { 55 | Assert.notNull(req, 'IdentifierRsp.create() "req" param must not be null'); 56 | 57 | try { 58 | const createRsp = await this.client.identifierCreate(userId, req); 59 | const createResponse = createRsp.data; 60 | 61 | if (isErrorRsp(createResponse)) { 62 | throw new BaseError( 63 | 'ErrorRsp', 64 | httpStatusCodes.AUTH_RSP_ERROR.code, 65 | httpStatusCodes.AUTH_RSP_ERROR.description, 66 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 67 | ); 68 | } 69 | 70 | return createResponse; 71 | } catch (error) { 72 | throw Helper.convertToServerError(error, 'IdentifierRsp.create()'); 73 | } 74 | } 75 | 76 | async delete(userId: string, identifierId: string): Promise { 77 | Assert.notEmptyString(userId, 'Identifier.delete() "id" param must not be empty'); 78 | Assert.notEmptyString(identifierId, 'Identifier.delete() "identifierId" param must not be empty'); 79 | 80 | try { 81 | const deleteRsp = await this.client.identifierDelete(userId, identifierId); 82 | const deleteResponse = deleteRsp.data; 83 | 84 | if (isErrorRsp(deleteResponse)) { 85 | throw new BaseError( 86 | 'ErrorRsp', 87 | httpStatusCodes.AUTH_RSP_ERROR.code, 88 | httpStatusCodes.AUTH_RSP_ERROR.description, 89 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 90 | ); 91 | } 92 | 93 | return deleteResponse; 94 | } catch (error) { 95 | throw Helper.convertToServerError(error, 'Identifier.delete()'); 96 | } 97 | } 98 | 99 | async list(filter?: string[], sort = '', page = 1, pageSize = 10): Promise { 100 | try { 101 | const listRsp = await this.client.identifierList(sort, filter, page, pageSize); 102 | const listResponse = listRsp.data; 103 | 104 | if (isErrorRsp(listResponse)) { 105 | throw new BaseError( 106 | 'Identifier list ErrorRsp', 107 | httpStatusCodes.AUTH_RSP_ERROR.code, 108 | httpStatusCodes.AUTH_RSP_ERROR.description, 109 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 110 | ); 111 | } 112 | 113 | return listResponse; 114 | } catch (error) { 115 | throw Helper.convertToServerError(error, 'Identifier.list()'); 116 | } 117 | } 118 | 119 | async listByValueAndType( 120 | value: string, 121 | type: IdentifierType, 122 | sort?: string, 123 | page?: number, 124 | pageSize?: number, 125 | ): Promise { 126 | return this.list([`identifierValue:eq:${value}`, `identifierType:eq:${type}`], sort, page, pageSize); 127 | } 128 | 129 | async listByUserId(userId: string, sort?: string, page?: number, pageSize?: number): Promise { 130 | let id = userId; 131 | 132 | // filter queries are using userID without prefix 133 | if (userId.startsWith(USER_ID_PREFIX)) { 134 | id = userId.substring(USER_ID_PREFIX.length); 135 | } 136 | 137 | return this.list([`userID:eq:${id}`], sort, page, pageSize); 138 | } 139 | 140 | listByUserIdAndType( 141 | userId: string, 142 | type: IdentifierType, 143 | sort?: string, 144 | page?: number, 145 | pageSize?: number, 146 | ): Promise { 147 | let id = userId; 148 | 149 | // filter queries are using userID without prefix 150 | if (userId.startsWith(USER_ID_PREFIX)) { 151 | id = userId.substring(USER_ID_PREFIX.length); 152 | } 153 | 154 | return this.list([`userID:eq:${id}`, `identifierType:eq:${type}`], sort, page, pageSize); 155 | } 156 | 157 | async updateIdentifier( 158 | userId: string, 159 | identifierId: string, 160 | identifierUpdateReq: IdentifierUpdateReq, 161 | ): Promise { 162 | Assert.notEmptyString(userId, 'Identifier.update() "userId" param must not be empty'); 163 | Assert.notEmptyString(identifierId, 'Identifier.update() "identifierId" param must not be empty'); 164 | Assert.notNull(identifierUpdateReq, 'Identifier.update() "identifierUpdateReq" param must not be null'); 165 | 166 | try { 167 | const updateRsp = await this.client.identifierUpdate(userId, identifierId, identifierUpdateReq); 168 | const updateResponse = updateRsp.data; 169 | 170 | if (isErrorRsp(updateResponse)) { 171 | throw new BaseError( 172 | 'ErrorRsp', 173 | httpStatusCodes.AUTH_RSP_ERROR.code, 174 | httpStatusCodes.AUTH_RSP_ERROR.description, 175 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 176 | ); 177 | } 178 | 179 | return updateResponse; 180 | } catch (error) { 181 | throw Helper.convertToServerError(error, 'Identifier.update()'); 182 | } 183 | } 184 | 185 | updateStatus(userId: string, identifierId: string, status: IdentifierStatus): Promise { 186 | return this.updateIdentifier(userId, identifierId, { status }); 187 | } 188 | } 189 | 190 | export default Identifier; 191 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import Identifier, { IdentifierInterface } from './identifierService.js'; 2 | import Session, { SessionInterface } from './sessionService.js'; 3 | import User, { UserInterface } from './userService.js'; 4 | 5 | export { Session, SessionInterface, User, UserInterface, Identifier, IdentifierInterface }; 6 | -------------------------------------------------------------------------------- /src/services/sessionService.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | /* eslint-disable class-methods-use-this */ 3 | import { createRemoteJWKSet, errors, JWTPayload, jwtVerify } from 'jose'; 4 | import { Assert } from '../helpers/index.js'; 5 | import ValidationError, { ValidationErrorNames } from '../errors/validationError.js'; 6 | import {JOSEAlgNotAllowed} from "jose/dist/types/util/errors"; 7 | 8 | export interface SessionInterface { 9 | validateToken(sessionToken: string): Promise<{ userId: string; fullName: string }>; 10 | } 11 | 12 | interface MyJWTPayload extends JWTPayload { 13 | name: string; 14 | iss: string; 15 | sub: string; 16 | } 17 | 18 | const MIN_SESSION_TOKEN_LENGTH = 10; 19 | 20 | class Session implements SessionInterface { 21 | private issuer: string; 22 | 23 | private cacheMaxAge: number; 24 | 25 | private jwkSet; 26 | 27 | private projectID: string; 28 | 29 | constructor(sessionTokenCookieName: string, issuer: string, jwksURI: string, cacheMaxAge: number, projectID: string) { 30 | if (!sessionTokenCookieName || !issuer || !jwksURI) { 31 | throw new Error('Required parameter is empty'); 32 | } 33 | 34 | this.issuer = issuer; 35 | this.cacheMaxAge = cacheMaxAge; 36 | this.jwkSet = createRemoteJWKSet(new URL(jwksURI), { 37 | cacheMaxAge: this.cacheMaxAge, 38 | cooldownDuration: this.cacheMaxAge, 39 | headers: { 'X-Corbado-ProjectID': projectID }, 40 | }); 41 | this.projectID = projectID; 42 | } 43 | 44 | /** 45 | * Validate the session token and return the user ID and full name 46 | * @param {any} sessionToken:string 47 | * @returns {any} { userId: string; fullName: string } 48 | */ 49 | public async validateToken(sessionToken: string): Promise<{ userId: string; fullName: string }> { 50 | Assert.notEmptyString(sessionToken, 'sessionToken not given'); 51 | 52 | if (sessionToken.length < MIN_SESSION_TOKEN_LENGTH) { 53 | throw new ValidationError(ValidationErrorNames.InvalidShortSession); 54 | } 55 | 56 | try { 57 | const { payload } = await jwtVerify(sessionToken, this.jwkSet); 58 | 59 | const { iss, name, sub } = payload as MyJWTPayload; 60 | 61 | this.validateIssuer(iss); 62 | 63 | return { 64 | userId: sub, 65 | fullName: name, 66 | }; 67 | } catch (error) { 68 | if (error instanceof errors.JWTClaimValidationFailed) { 69 | throw new ValidationError(ValidationErrorNames.JWTClaimValidationFailed); 70 | } 71 | 72 | if (error instanceof errors.JWTExpired) { 73 | throw new ValidationError(ValidationErrorNames.JWTExpired); 74 | } 75 | 76 | if (error instanceof errors.JWTInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSENotSupported) { 77 | throw new ValidationError(ValidationErrorNames.JWTInvalid); 78 | } 79 | 80 | throw error; 81 | } 82 | } 83 | 84 | private validateIssuer(jwtIssuer: string) { 85 | if (!jwtIssuer) { 86 | throw new ValidationError(ValidationErrorNames.EmptyIssuer, false); 87 | } 88 | 89 | if (jwtIssuer === `https://${this.projectID}.frontendapi.corbado.io`) { 90 | return; 91 | } 92 | 93 | if (jwtIssuer === `https://${this.projectID}.frontendapi.cloud.corbado.io`) { 94 | return; 95 | } 96 | 97 | if (jwtIssuer !== this.issuer) { 98 | throw new ValidationError( 99 | ValidationErrorNames.InvalidIssuer, 100 | false, 101 | `JWT issuer mismatch (configured trough FrontendAPI: '${this.issuer}', JWT issuer: '${jwtIssuer}')`, 102 | ); 103 | } 104 | } 105 | } 106 | 107 | export default Session; 108 | -------------------------------------------------------------------------------- /src/services/userService.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { BaseError, httpStatusCodes } from '../errors/index.js'; 3 | import { Assert, Helper, isErrorRsp } from '../helpers/index.js'; 4 | import { UserCreateReq, UsersApi, User, UserStatus, GenericRsp } from '../generated/api.js'; 5 | 6 | export interface UserInterface { 7 | create(req: UserCreateReq): Promise; 8 | createActiveByName(fullName: string): Promise; 9 | delete(id: string): Promise; 10 | get(id: string): Promise; 11 | } 12 | 13 | class UserService implements UserInterface { 14 | private client: UsersApi; 15 | 16 | constructor(axios: AxiosInstance) { 17 | Assert.notNull(axios, 'User Axios instance must not be null'); 18 | this.client = new UsersApi(undefined, '', axios); 19 | } 20 | 21 | async create(req: UserCreateReq): Promise { 22 | try { 23 | Assert.notNull(req, 'User.create() "req" param must not be null'); 24 | Assert.notEmptyString(req.fullName ? req.fullName : '', 'User.create() "fullName" param must not be empty'); 25 | Assert.notNull(req.status, 'User.create() "status" param must not be null'); 26 | 27 | const createRsp = await this.client.userCreate(req); 28 | const createResponse = createRsp.data; 29 | 30 | if (isErrorRsp(createResponse)) { 31 | throw new BaseError( 32 | 'ErrorRsp', 33 | httpStatusCodes.AUTH_RSP_ERROR.code, 34 | httpStatusCodes.AUTH_RSP_ERROR.description, 35 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 36 | ); 37 | } 38 | 39 | return createResponse; 40 | } catch (error) { 41 | throw Helper.convertToServerError(error, 'User.create()'); 42 | } 43 | } 44 | 45 | async createActiveByName(fullName: string): Promise { 46 | Assert.notEmptyString(fullName, 'User.create() "fullName" param must not be null'); 47 | 48 | const request: UserCreateReq = { 49 | fullName, 50 | status: UserStatus.Active, 51 | }; 52 | 53 | return this.create(request); 54 | } 55 | 56 | async delete(id: string): Promise { 57 | Assert.notEmptyString(id, 'User.delete() "id" param must not be empty'); 58 | 59 | try { 60 | const deleteRsp = await this.client.userDelete(id); 61 | const deleteResponse = deleteRsp.data; 62 | 63 | if (isErrorRsp(deleteResponse)) { 64 | throw new BaseError( 65 | 'ErrorRsp', 66 | httpStatusCodes.AUTH_RSP_ERROR.code, 67 | httpStatusCodes.AUTH_RSP_ERROR.description, 68 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 69 | ); 70 | } 71 | 72 | return deleteResponse; 73 | } catch (error) { 74 | throw Helper.convertToServerError(error, 'User.delete()'); 75 | } 76 | } 77 | 78 | async get(id: string): Promise { 79 | Assert.notEmptyString(id, 'User.get() "id" param must not be an empty string'); 80 | 81 | try { 82 | const getRsp = await this.client.userGet(id); 83 | const getResponse = getRsp.data; 84 | 85 | if (isErrorRsp(getResponse)) { 86 | throw new BaseError( 87 | 'User get ErrorRsp', 88 | httpStatusCodes.AUTH_RSP_ERROR.code, 89 | httpStatusCodes.AUTH_RSP_ERROR.description, 90 | httpStatusCodes.AUTH_RSP_ERROR.isOperational, 91 | ); 92 | } 93 | 94 | return getResponse; 95 | } catch (error) { 96 | throw Helper.convertToServerError(error, 'User.get()'); 97 | } 98 | } 99 | } 100 | 101 | export default UserService; 102 | -------------------------------------------------------------------------------- /src/specs/common.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | version: 1.0.0 5 | title: Corbado generic API entities 6 | description: Overview of all Corbado generic API entities. 7 | contact: 8 | name: Corbado API Team 9 | email: support@corbado.com 10 | url: https://www.corbado.com 11 | 12 | servers: 13 | - url: https://api.corbado.com 14 | 15 | tags: 16 | - name: Common 17 | description: Common entities 18 | 19 | paths: 20 | # This has to be there with all possible schemas references, so that the generator actually generates types for them 21 | /unused/{sessionID}: 22 | get: 23 | description: unused 24 | operationId: unused 25 | tags: 26 | - Common 27 | security: 28 | - projectID: [] 29 | parameters: 30 | - $ref: '#/components/parameters/remoteAddress' 31 | - $ref: '#/components/parameters/userAgent' 32 | - $ref: '#/components/parameters/sort' 33 | - $ref: '#/components/parameters/filter' 34 | - $ref: '#/components/parameters/page' 35 | - $ref: '#/components/parameters/pageSize' 36 | - $ref: '#/components/parameters/sessionID' 37 | responses: 38 | '200': 39 | description: unused 40 | content: 41 | application/json: 42 | schema: 43 | $ref: '#/components/schemas/allTypes' 44 | 45 | components: 46 | securitySchemes: 47 | projectID: 48 | in: header 49 | name: X-Corbado-ProjectID 50 | type: apiKey 51 | 52 | parameters: 53 | remoteAddress: 54 | name: remoteAddress 55 | in: query 56 | description: Client's remote address 57 | required: false 58 | schema: 59 | type: string 60 | 61 | userAgent: 62 | name: userAgent 63 | in: query 64 | description: Client's user agent 65 | required: false 66 | schema: 67 | type: string 68 | 69 | sort: 70 | name: sort 71 | in: query 72 | description: Field sorting 73 | required: false 74 | schema: 75 | type: string 76 | 77 | filter: 78 | name: filter[] 79 | in: query 80 | description: Field filtering 81 | required: false 82 | style: form 83 | explode: true 84 | schema: 85 | type: array 86 | items: 87 | type: string 88 | examples: 89 | filterEmail: 90 | summary: Filter for one email address 91 | value: 92 | - name:eq:mail@exammple.com 93 | filterTimepoint: 94 | summary: timePoint after 20/07/2021 95 | value: 96 | - timePoint:gt:2021-07-20T00:00:00 97 | 98 | page: 99 | name: page 100 | in: query 101 | description: Page number 102 | required: false 103 | schema: 104 | type: integer 105 | default: 1 106 | 107 | pageSize: 108 | name: pageSize 109 | in: query 110 | description: Number of items per page 111 | required: false 112 | schema: 113 | type: integer 114 | default: 10 115 | 116 | sessionID: 117 | name: sessionID 118 | in: path 119 | description: ID of session 120 | required: true 121 | schema: 122 | type: string 123 | minLength: 30 124 | maxLength: 30 125 | 126 | schemas: 127 | paging: 128 | type: object 129 | required: 130 | - page 131 | - totalPages 132 | - totalItems 133 | properties: 134 | page: 135 | description: current page returned in response 136 | type: integer 137 | default: 1 138 | totalPages: 139 | description: total number of pages available 140 | type: integer 141 | totalItems: 142 | description: total number of items available 143 | type: integer 144 | 145 | clientInfo: 146 | type: object 147 | required: 148 | - remoteAddress 149 | - userAgent 150 | properties: 151 | remoteAddress: 152 | description: client's IP address 153 | type: string 154 | example: '::ffff:172.18.0.1' 155 | userAgent: 156 | description: client's User Agent 157 | type: string 158 | example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' 159 | 160 | status: 161 | type: string 162 | enum: ['active', 'pending', 'deleted'] 163 | description: Generic status that can describe Corbado entities 164 | 165 | authMethods: 166 | type: array 167 | items: 168 | $ref: '#/components/schemas/authMethod' 169 | 170 | authMethod: 171 | type: string 172 | enum: ['email', 'phone_number', 'webauthn', 'password'] 173 | description: Authentication methods 174 | 175 | fullUser: 176 | type: object 177 | description: User entry with emails and phone numbers 178 | required: 179 | - ID 180 | - name 181 | - fullName 182 | - created 183 | - updated 184 | - status 185 | - emails 186 | - phoneNumbers 187 | - usernames 188 | - socialAccounts 189 | properties: 190 | ID: 191 | $ref: '#/components/schemas/userID' 192 | name: 193 | type: string 194 | fullName: 195 | type: string 196 | created: 197 | $ref: '#/components/schemas/created' 198 | updated: 199 | $ref: '#/components/schemas/updated' 200 | status: 201 | $ref: '#/components/schemas/status' 202 | emails: 203 | type: array 204 | items: 205 | $ref: '#/components/schemas/userEmail' 206 | phoneNumbers: 207 | type: array 208 | items: 209 | $ref: '#/components/schemas/userPhoneNumber' 210 | usernames: 211 | type: array 212 | items: 213 | $ref: '#/components/schemas/userUsername' 214 | socialAccounts: 215 | type: array 216 | items: 217 | $ref: '#/components/schemas/userSocialAccount' 218 | 219 | userEmail: 220 | type: object 221 | description: User's email 222 | required: 223 | - ID 224 | - email 225 | - created 226 | - updated 227 | - status 228 | properties: 229 | ID: 230 | $ref: '#/components/schemas/ID' 231 | email: 232 | type: string 233 | created: 234 | $ref: '#/components/schemas/created' 235 | updated: 236 | $ref: '#/components/schemas/updated' 237 | status: 238 | $ref: '#/components/schemas/status' 239 | 240 | userSocialAccount: 241 | type: object 242 | description: User's social account 243 | required: 244 | - providerType 245 | - identifierValue 246 | - avatarUrl 247 | - fullName 248 | properties: 249 | providerType: 250 | $ref: '#/components/schemas/socialProviderType' 251 | identifierValue: 252 | type: string 253 | avatarUrl: 254 | type: string 255 | fullName: 256 | type: string 257 | 258 | userPhoneNumber: 259 | type: object 260 | description: User's phone number 261 | required: 262 | - ID 263 | - phoneNumber 264 | - created 265 | - updated 266 | - status 267 | properties: 268 | ID: 269 | $ref: '#/components/schemas/ID' 270 | phoneNumber: 271 | type: string 272 | created: 273 | $ref: '#/components/schemas/created' 274 | updated: 275 | $ref: '#/components/schemas/updated' 276 | status: 277 | $ref: '#/components/schemas/status' 278 | 279 | userUsername: 280 | type: object 281 | description: User's username 282 | required: 283 | - ID 284 | - username 285 | - created 286 | - updated 287 | - status 288 | properties: 289 | ID: 290 | $ref: '#/components/schemas/ID' 291 | username: 292 | type: string 293 | created: 294 | $ref: '#/components/schemas/created' 295 | updated: 296 | $ref: '#/components/schemas/updated' 297 | status: 298 | $ref: '#/components/schemas/status' 299 | 300 | highEntropyValues: 301 | description: High entropy values from browser 302 | type: object 303 | required: 304 | - platform 305 | - platformVersion 306 | - mobile 307 | properties: 308 | platform: 309 | description: Platform 310 | type: string 311 | example: 'macOS' 312 | platformVersion: 313 | description: Platform version 314 | type: string 315 | example: '14.1.2' 316 | mobile: 317 | description: Mobile 318 | type: boolean 319 | 320 | ID: 321 | description: generic ID 322 | type: string 323 | 324 | userID: 325 | description: ID of the user 326 | type: string 327 | 328 | deviceID: 329 | description: ID of the device 330 | type: string 331 | 332 | emailID: 333 | description: ID of the email 334 | type: string 335 | 336 | phoneNumberID: 337 | description: ID of the phone number 338 | type: string 339 | 340 | projectID: 341 | description: ID of project 342 | type: string 343 | 344 | requestID: 345 | description: Unique ID of request, you can provide your own while making the request, if not the ID will be randomly generated on server side 346 | type: string 347 | example: 'req-557...663' 348 | 349 | emailLinkID: 350 | description: ID of the email magic link 351 | type: string 352 | 353 | emailCodeID: 354 | description: ID of the email OTP 355 | type: string 356 | 357 | additionalPayload: 358 | description: Additional payload in JSON format 359 | type: string 360 | example: '{"projectAbbreviation":"CRBD"}' 361 | 362 | created: 363 | description: Timestamp of when the entity was created in yyyy-MM-dd'T'HH:mm:ss format 364 | type: string 365 | 366 | updated: 367 | description: Timestamp of when the entity was last updated in yyyy-MM-dd'T'HH:mm:ss format 368 | type: string 369 | 370 | deleted: 371 | description: Timestamp of when the entity was deleted in yyyy-MM-dd'T'HH:mm:ss format 372 | type: string 373 | 374 | loginIdentifierType: 375 | description: Login Identifier type 376 | type: string 377 | enum: ['email', 'phone_number', 'custom'] 378 | 379 | appType: 380 | description: Application type 381 | type: string 382 | enum: ['empty', 'web', 'native'] 383 | 384 | sessionManagement: 385 | description: What session management should be used 386 | type: string 387 | enum: ['SessionManagementCorbado', 'SessionManagementOwn'] 388 | 389 | requestData: 390 | description: Data about the request itself, can be used for debugging 391 | type: object 392 | required: 393 | - requestID 394 | - link 395 | properties: 396 | requestID: 397 | $ref: '#/components/schemas/requestID' 398 | link: 399 | description: Link to dashboard with details about request 400 | type: string 401 | example: 'https://my.corbado.com/requests/req-xxxxxxxxxxxxxxxxxxx' 402 | 403 | loginIdentifierConfig: 404 | type: object 405 | required: 406 | - type 407 | - enforceVerification 408 | - useAsLoginIdentifier 409 | properties: 410 | type: 411 | $ref: '#/components/schemas/loginIdentifierType' 412 | enforceVerification: 413 | type: string 414 | enum: [none, signup, at_first_login] 415 | useAsLoginIdentifier: 416 | type: boolean 417 | metadata: 418 | type: object 419 | 420 | socialProviderType: 421 | type: string 422 | enum: ['google', 'microsoft', 'github'] 423 | 424 | # this is necessary so that code generator doesn't ignore "unused" types 425 | allTypes: 426 | type: object 427 | properties: 428 | p1: 429 | $ref: '#/components/schemas/paging' 430 | p2: 431 | $ref: '#/components/schemas/clientInfo' 432 | p3: 433 | $ref: '#/components/schemas/ID' 434 | p4: 435 | $ref: '#/components/schemas/userID' 436 | p5: 437 | $ref: '#/components/schemas/emailID' 438 | p6: 439 | $ref: '#/components/schemas/emailLinkID' 440 | p7: 441 | $ref: '#/components/schemas/phoneNumberID' 442 | p8: 443 | $ref: '#/components/schemas/created' 444 | p9: 445 | $ref: '#/components/schemas/updated' 446 | p10: 447 | $ref: '#/components/schemas/deleted' 448 | p11: 449 | $ref: '#/components/schemas/deviceID' 450 | p12: 451 | $ref: '#/components/schemas/additionalPayload' 452 | p13: 453 | $ref: '#/components/schemas/status' 454 | p14: 455 | $ref: '#/components/schemas/projectID' 456 | p15: 457 | $ref: '#/components/schemas/requestID' 458 | p16: 459 | $ref: '#/components/schemas/errorRsp' 460 | p17: 461 | $ref: '#/components/schemas/authMethods' 462 | p18: 463 | $ref: '#/components/schemas/fullUser' 464 | p19: 465 | $ref: '#/components/schemas/loginIdentifierType' 466 | p20: 467 | $ref: '#/components/schemas/emailCodeID' 468 | p21: 469 | $ref: '#/components/schemas/appType' 470 | p22: 471 | $ref: '#/components/schemas/sessionManagement' 472 | p23: 473 | $ref: '#/components/schemas/highEntropyValues' 474 | p24: 475 | $ref: '#/components/schemas/loginIdentifierConfig' 476 | p25: 477 | $ref: '#/components/schemas/socialProviderType' 478 | 479 | genericRsp: 480 | type: object 481 | required: 482 | - httpStatusCode 483 | - message 484 | - requestData 485 | - runtime 486 | properties: 487 | httpStatusCode: 488 | description: HTTP status code of operation 489 | type: integer 490 | format: int32 491 | minimum: 200 492 | maximum: 599 493 | message: 494 | type: string 495 | example: 'OK' 496 | requestData: 497 | $ref: '#/components/schemas/requestData' 498 | runtime: 499 | description: Runtime in seconds for this request 500 | type: number 501 | format: float 502 | example: 0.06167686 503 | 504 | errorRsp: 505 | allOf: 506 | - $ref: '#/components/schemas/genericRsp' 507 | - type: object 508 | required: 509 | - error 510 | properties: 511 | data: 512 | type: object 513 | error: 514 | type: object 515 | required: 516 | - type 517 | - links 518 | properties: 519 | type: 520 | description: Type of error 521 | type: string 522 | details: 523 | description: Details of error 524 | type: string 525 | validation: 526 | description: Validation errors per field 527 | type: array 528 | items: 529 | type: object 530 | required: 531 | - field 532 | - message 533 | properties: 534 | field: 535 | type: string 536 | message: 537 | type: string 538 | links: 539 | description: Additional links to help understand the error 540 | type: array 541 | items: 542 | type: string 543 | -------------------------------------------------------------------------------- /src/webhook/entities/authMethodsDataRequest.ts: -------------------------------------------------------------------------------- 1 | class AuthMethodsDataRequest { 2 | username: string; 3 | 4 | constructor(username: string) { 5 | this.username = username; 6 | } 7 | } 8 | 9 | export default AuthMethodsDataRequest; 10 | -------------------------------------------------------------------------------- /src/webhook/entities/authMethodsDataResponse.ts: -------------------------------------------------------------------------------- 1 | export enum AuthMethodsDataResponseStatusEnum { 2 | USER_EXISTS = 'exists', 3 | USER_NOT_EXISTS = 'not_exists', 4 | USER_BLOCKED = 'blocked', 5 | } 6 | 7 | export class AuthMethodsDataResponse { 8 | status: AuthMethodsDataResponseStatusEnum; 9 | 10 | constructor(status: AuthMethodsDataResponseStatusEnum) { 11 | this.status = status; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/webhook/entities/authMethodsRequest.ts: -------------------------------------------------------------------------------- 1 | import CommonRequest from './commonRequest.js'; 2 | 3 | class AuthMethodsRequest extends CommonRequest { 4 | data: string; 5 | 6 | constructor(data: string, id: string, projectId: string, action: string, requestId: string) { 7 | super(id, projectId, action, requestId); 8 | this.data = data; 9 | } 10 | } 11 | 12 | export default AuthMethodsRequest; 13 | -------------------------------------------------------------------------------- /src/webhook/entities/authMethodsResponse.ts: -------------------------------------------------------------------------------- 1 | import { AuthMethodsDataResponseStatusEnum } from './authMethodsDataResponse.js'; 2 | import CommonResponse from './commonResponse.js'; 3 | 4 | class AuthMethodsResponse extends CommonResponse { 5 | data: AuthMethodsDataResponseStatusEnum; 6 | 7 | constructor(data: AuthMethodsDataResponseStatusEnum, responseId: string) { 8 | super(responseId); 9 | this.data = data; 10 | } 11 | } 12 | 13 | export default AuthMethodsResponse; 14 | -------------------------------------------------------------------------------- /src/webhook/entities/commonRequest.ts: -------------------------------------------------------------------------------- 1 | class CommonRequest { 2 | id: string; 3 | 4 | projectID: string; 5 | 6 | action: string; 7 | 8 | /** 9 | * @deprecated 10 | */ 11 | requestID: string; 12 | 13 | constructor(id: string, projectID: string, action: string, requestID: string) { 14 | this.id = id; 15 | this.projectID = projectID; 16 | this.action = action; 17 | this.requestID = requestID; 18 | } 19 | } 20 | 21 | export default CommonRequest; 22 | -------------------------------------------------------------------------------- /src/webhook/entities/commonResponse.ts: -------------------------------------------------------------------------------- 1 | class CommonResponse { 2 | responseId: string; 3 | 4 | constructor(responseId: string) { 5 | this.responseId = responseId; 6 | } 7 | } 8 | 9 | export default CommonResponse; 10 | -------------------------------------------------------------------------------- /src/webhook/entities/index.ts: -------------------------------------------------------------------------------- 1 | import AuthMethodsDataRequest from './authMethodsDataRequest.js'; 2 | import { AuthMethodsDataResponse, AuthMethodsDataResponseStatusEnum } from './authMethodsDataResponse.js'; 3 | import AuthMethodsRequest from './authMethodsRequest.js'; 4 | import AuthMethodsResponse from './authMethodsResponse.js'; 5 | import CommonRequest from './commonRequest.js'; 6 | import CommonResponse from './commonResponse.js'; 7 | import PasswordVerifyDataRequest from './passwordVerifyDataRequest.js'; 8 | import PasswordVerifyDataResponse from './passwordVerifyDataResponse.js'; 9 | import PasswordVerifyResponse from './passwordVerifyResponse.js'; 10 | import PasswordVerifyRequest from './passwordVerifyRequest.js'; 11 | 12 | export { 13 | AuthMethodsDataRequest, 14 | AuthMethodsDataResponse, 15 | AuthMethodsDataResponseStatusEnum, 16 | AuthMethodsRequest, 17 | AuthMethodsResponse, 18 | CommonRequest, 19 | CommonResponse, 20 | PasswordVerifyRequest, 21 | PasswordVerifyResponse, 22 | PasswordVerifyDataRequest, 23 | PasswordVerifyDataResponse, 24 | }; 25 | -------------------------------------------------------------------------------- /src/webhook/entities/passwordVerifyDataRequest.ts: -------------------------------------------------------------------------------- 1 | class PasswordVerifyDataRequest { 2 | username: string; 3 | 4 | password: string; 5 | 6 | constructor(username: string, password: string) { 7 | this.username = username; 8 | this.password = password; 9 | } 10 | } 11 | 12 | export default PasswordVerifyDataRequest; 13 | -------------------------------------------------------------------------------- /src/webhook/entities/passwordVerifyDataResponse.ts: -------------------------------------------------------------------------------- 1 | class PasswordVerifyResponse { 2 | success: boolean; 3 | 4 | constructor(success: boolean) { 5 | this.success = success; 6 | } 7 | } 8 | 9 | export default PasswordVerifyResponse; 10 | -------------------------------------------------------------------------------- /src/webhook/entities/passwordVerifyRequest.ts: -------------------------------------------------------------------------------- 1 | import CommonRequest from './commonRequest.js'; 2 | 3 | class PasswordVerifyRequest extends CommonRequest { 4 | data: string; 5 | 6 | constructor(data: string, id: string, projectId: string, action: string, requestId: string) { 7 | super(id, projectId, action, requestId); 8 | this.data = data; 9 | } 10 | } 11 | 12 | export default PasswordVerifyRequest; 13 | -------------------------------------------------------------------------------- /src/webhook/entities/passwordVerifyResponse.ts: -------------------------------------------------------------------------------- 1 | import CommonResponse from './commonResponse.js'; 2 | 3 | class PasswordVerifyResponse extends CommonResponse { 4 | data: string; 5 | 6 | constructor(data: string, responseId: string) { 7 | super(responseId); 8 | this.data = data; 9 | } 10 | } 11 | 12 | export default PasswordVerifyResponse; 13 | -------------------------------------------------------------------------------- /src/webhook/webhook.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { BaseError, httpStatusCodes } from '../errors/index.js'; 3 | import { Assert, Helper } from '../helpers/index.js'; 4 | import { 5 | AuthMethodsRequest, 6 | AuthMethodsDataRequest, 7 | AuthMethodsDataResponse, 8 | AuthMethodsDataResponseStatusEnum, 9 | AuthMethodsResponse, 10 | PasswordVerifyRequest, 11 | PasswordVerifyDataRequest, 12 | PasswordVerifyDataResponse, 13 | PasswordVerifyResponse, 14 | } from './entities/index.js'; 15 | 16 | interface NestedBody { 17 | id: string; 18 | projectID: string; 19 | action: string; 20 | data: { username: string; password: string }; 21 | } 22 | 23 | interface Body { 24 | id: string; 25 | projectID: string; 26 | action: string; 27 | data: NestedBody; 28 | } 29 | 30 | interface RequestWithBody extends Request { 31 | body: Body; 32 | } 33 | 34 | export enum AuthMethodsDataResponseStatus { 35 | ACTION_AUTH_METHODS = 'auth_methods', 36 | ACTION_PASSWORD_VERIFY = 'password_verify', 37 | } 38 | 39 | export const StandardFields = ['id', 'projectID', 'action', 'data']; 40 | 41 | class Webhook { 42 | private username: string; 43 | 44 | private password: string; 45 | 46 | private automaticAuthenticationHandling = true; 47 | 48 | private authenticated = false; 49 | 50 | constructor(username: string, password: string) { 51 | Assert.notEmptyString(username, 'Webhook instance "username" param must not be an empty string'); 52 | Assert.notEmptyString(password, 'Webhook instance "password" param must not be an empty string'); 53 | 54 | this.username = username; 55 | this.password = password; 56 | } 57 | 58 | disableAutomaticAuthenticationHandling(): void { 59 | this.automaticAuthenticationHandling = false; 60 | } 61 | 62 | // TODO: 63 | // This implementation came from eyeballing our PHP SDK 64 | // Verify that it matches its functionality 1:1 65 | checkAuthentication(req: Request): boolean { 66 | const authHeader = req.headers.authorization || ''; 67 | const base64Credentials = authHeader.split(' ')[1] || ''; 68 | const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii'); 69 | const [username, password] = credentials.split(':'); 70 | 71 | return username === this.username && password === this.password; 72 | } 73 | 74 | static sendUnauthorizedResponse(res: Response, exit = true): void { 75 | res.status(401).header('WWW-Authenticate', 'Basic realm="Webhook"').send('Unauthorized.'); 76 | 77 | if (exit) { 78 | res.end(); 79 | } 80 | } 81 | 82 | handleAuthentication(req: Request, res: Response): void { 83 | if (this.authenticated) { 84 | throw new BaseError( 85 | 'Already authenticated', 86 | httpStatusCodes.USER_ALREADY_AUTHENTICATED.code, 87 | httpStatusCodes.USER_ALREADY_AUTHENTICATED.description, 88 | httpStatusCodes.USER_ALREADY_AUTHENTICATED.isOperational, 89 | ); 90 | } 91 | 92 | if (this.checkAuthentication(req)) { 93 | this.authenticated = true; 94 | return; 95 | } 96 | 97 | Webhook.sendUnauthorizedResponse(res); 98 | } 99 | 100 | checkAutomaticAuthentication(): void { 101 | if (this.authenticated || !this.automaticAuthenticationHandling) { 102 | return; 103 | } 104 | 105 | throw new BaseError( 106 | 'Missing authentication, call handleAuthentication() first', 107 | httpStatusCodes.USER_NOT_AUTHENTICATED.code, 108 | httpStatusCodes.USER_NOT_AUTHENTICATED.description, 109 | httpStatusCodes.USER_NOT_AUTHENTICATED.isOperational, 110 | ); 111 | } 112 | 113 | isPost(req: Request): boolean { 114 | this.checkAutomaticAuthentication(); 115 | 116 | return req.method === 'POST'; 117 | } 118 | 119 | getAction(req: Request): string { 120 | this.checkAutomaticAuthentication(); 121 | 122 | const actionHeader = req.headers['x-corbado-action']; 123 | if (!actionHeader) { 124 | throw new BaseError( 125 | 'Missing action header', 126 | httpStatusCodes.MISSING_ACTION_HEADER.code, 127 | httpStatusCodes.MISSING_ACTION_HEADER.description, 128 | httpStatusCodes.MISSING_ACTION_HEADER.isOperational, 129 | ); 130 | } 131 | 132 | switch (actionHeader) { 133 | case 'authMethods': 134 | return AuthMethodsDataResponseStatus.ACTION_AUTH_METHODS; 135 | 136 | case 'passwordVerify': 137 | return AuthMethodsDataResponseStatus.ACTION_PASSWORD_VERIFY; 138 | 139 | default: 140 | throw new BaseError( 141 | `Invalid action ("${actionHeader}")`, 142 | httpStatusCodes.INVALID_ACTION_HEADER.code, 143 | httpStatusCodes.INVALID_ACTION_HEADER.description, 144 | httpStatusCodes.INVALID_ACTION_HEADER.isOperational, 145 | ); 146 | } 147 | } 148 | 149 | getAuthMethodsRequest(req: RequestWithBody): AuthMethodsRequest { 150 | this.checkAutomaticAuthentication(); 151 | 152 | const body = Webhook.getRequestBody(req); 153 | const data = Helper.jsonDecode(JSON.stringify(body)) as unknown as NestedBody; 154 | Assert.keysInObject( 155 | StandardFields, 156 | data as unknown as Record, 157 | 'Webhook.getAuthMethodsRequest() body does not contain all required fields: id, projectID, action, data', 158 | ); 159 | Assert.keysInObject( 160 | ['username'], 161 | data.data as Record, 162 | `Webhook.getAuthMethodsRequest() body.data does not contain all required field: "username"`, 163 | ); 164 | 165 | const dataRequest = new AuthMethodsDataRequest(data.data.username); 166 | return new AuthMethodsRequest( 167 | data.id, 168 | data.projectID, 169 | AuthMethodsDataResponseStatus.ACTION_AUTH_METHODS, 170 | String(dataRequest), 171 | '', // This is for the deprcated 'requestId' 172 | ); 173 | } 174 | 175 | sendAuthMethodsResponse( 176 | res: Response, 177 | status: AuthMethodsDataResponseStatusEnum, 178 | exit = true, 179 | responseID = '', 180 | ): void { 181 | Assert.stringInSet( 182 | status, 183 | [ 184 | AuthMethodsDataResponseStatusEnum.USER_BLOCKED, 185 | AuthMethodsDataResponseStatusEnum.USER_EXISTS, 186 | AuthMethodsDataResponseStatusEnum.USER_NOT_EXISTS, 187 | ], 188 | 'Webhook.sendAuthMethodsResponse() status must be one of the AuthMethodsDataResponseStatusEnum values', 189 | ); 190 | 191 | this.checkAutomaticAuthentication(); 192 | 193 | const dataResponse = new AuthMethodsDataResponse(status); 194 | 195 | const response = new AuthMethodsResponse(dataResponse.status, responseID); 196 | 197 | Webhook.sendResponse(res, response); 198 | 199 | if (exit) { 200 | res.end(); 201 | } 202 | } 203 | 204 | getPasswordVerifyRequest(req: RequestWithBody): PasswordVerifyRequest { 205 | this.checkAutomaticAuthentication(); 206 | 207 | const { body } = req; 208 | const data = Helper.jsonDecode(JSON.stringify(body)) as unknown as NestedBody; 209 | Assert.keysInObject( 210 | StandardFields, 211 | data as unknown as Record, 212 | 'Webhook.getPasswordVerifyRequest() body does not contain all required fields: id, projectID, action, data', 213 | ); 214 | Assert.keysInObject( 215 | ['username', 'password'], 216 | data.data as Record, 217 | 'Webhook.getPasswordVerifyRequest() body.data does not contain all required fields: username, password', 218 | ); 219 | 220 | const dataRequest = new PasswordVerifyDataRequest(data.data.username, data.data.password); 221 | 222 | const request = new PasswordVerifyRequest( 223 | data.id, 224 | data.projectID, 225 | AuthMethodsDataResponseStatus.ACTION_PASSWORD_VERIFY, 226 | String(dataRequest), 227 | '', // This is for the deprcated 'requestId' 228 | ); 229 | 230 | return request; 231 | } 232 | 233 | sendPasswordVerifyResponse(res: Response, success: boolean, exit = true, responseID = ''): void { 234 | this.checkAutomaticAuthentication(); 235 | 236 | const dataResponse = new PasswordVerifyDataResponse(success); 237 | 238 | const response = new PasswordVerifyResponse(String(dataResponse), responseID); 239 | 240 | Webhook.sendResponse(res, response); 241 | 242 | if (exit) { 243 | res.end(); 244 | } 245 | } 246 | 247 | static getRequestBody(req: RequestWithBody): string { 248 | if (!req.body) { 249 | throw new BaseError( 250 | 'Request body not found', 251 | httpStatusCodes.BAD_REQUEST.code, 252 | httpStatusCodes.BAD_REQUEST.description, 253 | httpStatusCodes.BAD_REQUEST.isOperational, 254 | ); 255 | } 256 | 257 | // TODO: 258 | // Verify and adjust based on how our middleware 259 | // is set up and what we expect the body content to be. 260 | return typeof req.body === 'object' ? JSON.stringify(req.body) : req.body; 261 | } 262 | 263 | static sendResponse(res: Response, response: unknown): void { 264 | res.type('application/json; charset=utf-8').send(Helper.jsonEncode(response)); 265 | } 266 | } 267 | 268 | export default Webhook; 269 | -------------------------------------------------------------------------------- /tests/config.test.ts: -------------------------------------------------------------------------------- 1 | import {DefaultCacheMaxAge, DefaultSessionTokenCookieName} from '../src/config.js'; 2 | import { BaseError } from '../src/errors/index.js'; 3 | import { Config } from '../src/index.js'; 4 | 5 | describe('Configuration class', () => { 6 | let projectID: string; 7 | let apiSecret: string; 8 | let frontendAPI: string; 9 | let backendAPI: string; 10 | 11 | beforeEach(() => { 12 | projectID = process.env.CORBADO_PROJECT_ID as string; // necessary to mitigate TS error 13 | apiSecret = process.env.CORBADO_API_SECRET as string; // Same here 14 | frontendAPI = process.env.CORBADO_FRONTEND_API as string; // Same here 15 | backendAPI = process.env.CORBADO_BACKEND_API as string; // Same here 16 | 17 | if (!projectID || !apiSecret) { 18 | throw new BaseError('Env Error', 5001, 'Both projectID and apiSecret must be defined', true); 19 | } 20 | }); 21 | 22 | const createAndAssertConfig = (config: Config) => { 23 | expect(config).toBeInstanceOf(Config); 24 | expect(config.ProjectID).toBe(projectID); 25 | expect(config.APISecret).toBe(apiSecret); 26 | expect(config.FrontendAPI).toBe(`https://${projectID}.frontendapi.cloud.corbado.io`); 27 | expect(config.BackendAPI).toBe(backendAPI); 28 | expect(config.SessionTokenCookieName).toBe(DefaultSessionTokenCookieName); 29 | expect(config.CacheMaxAge).toBe(DefaultCacheMaxAge); 30 | }; 31 | 32 | it('should instantiate Configuration with valid project ID and API secret and APIs', () => { 33 | const config = new Config(projectID, apiSecret, frontendAPI, backendAPI); 34 | createAndAssertConfig(config); 35 | }); 36 | 37 | it('should assign default values to BackendAPI, ShortSessionCookieName, CacheMaxAge, and JWTIssuer', () => { 38 | const config = new Config(projectID, apiSecret, frontendAPI, backendAPI); 39 | expect(config.BackendAPI).toBe(backendAPI); 40 | expect(config.FrontendAPI).toBe(frontendAPI); 41 | expect(config.SessionTokenCookieName).toBe(DefaultSessionTokenCookieName); 42 | expect(config.CacheMaxAge).toBe(DefaultCacheMaxAge); 43 | }); 44 | 45 | it('should throw an error when instantiated with an invalid project ID', () => { 46 | expect(() => new Config('invalid', apiSecret, frontendAPI, backendAPI)).toThrow( 47 | 'ProjectID must not be empty and must start with "pro-".', 48 | ); 49 | }); 50 | 51 | it('should throw an error when instantiated with an invalid API secret', () => { 52 | expect(() => new Config(projectID, 'invalid', frontendAPI, backendAPI)).toThrow( 53 | 'APISecret must not be empty and must start with "corbado1_".', 54 | ); 55 | }); 56 | 57 | it('should throw an error when project ID is undefined', () => { 58 | expect(() => new Config(undefined as unknown as string, apiSecret, frontendAPI, backendAPI)).toThrow( 59 | 'ProjectID must not be empty and must start with "pro-".', 60 | ); 61 | }); 62 | 63 | it('should throw an error when frontendAPI is wrong', () => { 64 | expect(() => new Config(projectID, apiSecret, `${frontendAPI}/v2`, backendAPI)).toThrow('path needs to be empty'); 65 | }); 66 | 67 | it('should throw an error when backendAPI is wrong', () => { 68 | expect(() => new Config(projectID, apiSecret, frontendAPI, `${backendAPI}/v2`)).toThrow('path needs to be empty'); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/integration/services/identifier.test.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from '../../../src'; 2 | import { ServerError } from '../../../src/errors'; 3 | import { 4 | IdentifierCreateReq, 5 | IdentifierStatus, 6 | IdentifierType, 7 | Identifier as IdentifierRsp, 8 | } from '../../../src/generated'; 9 | import Utils from '../../utils'; 10 | 11 | describe('Identifier Service Tests', () => { 12 | let sdk: SDK; 13 | let TEST_USER_ID: string; 14 | let TEST_USER_EMAIL: string; 15 | let TEST_USER_EMAIL_IDENTIFIER: IdentifierRsp; 16 | 17 | beforeAll(async () => { 18 | sdk = Utils.SDK(); 19 | 20 | // Create a test user and email identifier 21 | TEST_USER_ID = (await Utils.createUser()).userID; 22 | TEST_USER_EMAIL = Utils.createRandomTestEmail(); 23 | 24 | // Create an email identifier for the user 25 | TEST_USER_EMAIL_IDENTIFIER = await sdk.identifiers().create(TEST_USER_ID, { 26 | identifierType: IdentifierType.Email, 27 | identifierValue: TEST_USER_EMAIL, 28 | status: IdentifierStatus.Primary, 29 | }); 30 | }); 31 | 32 | test('should throw error on empty identifier creation', async () => { 33 | expect.assertions(3); 34 | 35 | const userId = (await Utils.createUser()).userID; 36 | const req: IdentifierCreateReq = { 37 | identifierType: IdentifierType.Email, 38 | identifierValue: '', // Empty email value 39 | status: IdentifierStatus.Primary, 40 | }; 41 | 42 | try { 43 | await sdk.identifiers().create(userId, req); 44 | } catch (error) { 45 | expect(error).toBeInstanceOf(ServerError); 46 | expect((error as ServerError).httpStatusCode).toEqual(400); 47 | expect((error as ServerError).getValidationMessages()).toEqual(['identifierValue: cannot be blank']); 48 | } 49 | }); 50 | 51 | test('should successfully create an identifier', async () => { 52 | const userId = (await Utils.createUser()).userID; 53 | const email = Utils.createRandomTestEmail(); 54 | const req: IdentifierCreateReq = { 55 | identifierType: IdentifierType.Email, 56 | identifierValue: email, 57 | status: IdentifierStatus.Primary, 58 | }; 59 | 60 | const createdIdentifier = await sdk.identifiers().create(userId, req); 61 | expect(createdIdentifier.userID).toEqual(userId); 62 | expect(createdIdentifier.value).toEqual(req.identifierValue); 63 | expect(createdIdentifier.type).toEqual(IdentifierType.Email); 64 | }); 65 | 66 | test('should list all identifiers by userId', async () => { 67 | const ret = await sdk.identifiers().listByUserId(TEST_USER_ID); 68 | const identifierExists = ret.identifiers.some( 69 | (identifier) => identifier.identifierID === TEST_USER_EMAIL_IDENTIFIER.identifierID, 70 | ); 71 | expect(identifierExists).toBe(true); 72 | expect(ret.identifiers.length).toEqual(1); // Only email identifier created so far 73 | }); 74 | 75 | test('should list identifiers by value and type', async () => { 76 | const ret = await sdk.identifiers().listByValueAndType(TEST_USER_EMAIL, IdentifierType.Email); 77 | expect(ret.identifiers.length).toBeGreaterThan(0); 78 | expect(ret.identifiers[0].value).toEqual(TEST_USER_EMAIL); 79 | }); 80 | 81 | test('should list identifiers by value and type with paging', async () => { 82 | const ret = await sdk.identifiers().listByValueAndType(TEST_USER_EMAIL, IdentifierType.Email, undefined, 1, 10); 83 | expect(ret.identifiers.length).toBeGreaterThan(0); 84 | expect(ret.identifiers[0].value).toEqual(TEST_USER_EMAIL); 85 | }); 86 | 87 | test('should list all identifiers', async () => { 88 | const ret = await sdk.identifiers().list(undefined, undefined, 1, 100); 89 | expect(ret).not.toBeNull(); 90 | }); 91 | 92 | test('should update identifier status successfully', async () => { 93 | await sdk 94 | .identifiers() 95 | .updateStatus( 96 | TEST_USER_EMAIL_IDENTIFIER.userID, 97 | TEST_USER_EMAIL_IDENTIFIER.identifierID, 98 | IdentifierStatus.Pending, 99 | ); 100 | 101 | let ret = await sdk 102 | .identifiers() 103 | .listByValueAndType(TEST_USER_EMAIL_IDENTIFIER.value, TEST_USER_EMAIL_IDENTIFIER.type); 104 | expect(ret.identifiers[0].status).toEqual(IdentifierStatus.Pending); 105 | 106 | await sdk 107 | .identifiers() 108 | .updateStatus( 109 | TEST_USER_EMAIL_IDENTIFIER.userID, 110 | TEST_USER_EMAIL_IDENTIFIER.identifierID, 111 | IdentifierStatus.Primary, 112 | ); 113 | 114 | ret = await sdk.identifiers().listByValueAndType(TEST_USER_EMAIL_IDENTIFIER.value, TEST_USER_EMAIL_IDENTIFIER.type); 115 | expect(ret.identifiers[0].status).toEqual(IdentifierStatus.Primary); 116 | }); 117 | 118 | test('should list all emails by userId', async () => { 119 | const testSize = 3; 120 | 121 | // Create multiple email identifiers for the same user 122 | const promises: Promise[] = []; 123 | 124 | [...Array(testSize).keys()].forEach(() => { 125 | promises.push( 126 | sdk.identifiers().create(TEST_USER_ID, { 127 | identifierType: IdentifierType.Email, 128 | identifierValue: Utils.createRandomTestEmail(), 129 | status: IdentifierStatus.Verified, 130 | }), 131 | ); 132 | }); 133 | 134 | await Promise.all(promises); 135 | 136 | const allEmails = await sdk 137 | .identifiers() 138 | .listByUserIdAndType(TEST_USER_ID, IdentifierType.Email, undefined, undefined); 139 | expect(allEmails.identifiers.length).toEqual(testSize + 1); // One email was already created before 140 | }); 141 | 142 | test('should successfully delete an identifier', async () => { 143 | expect.assertions(2); 144 | 145 | const identifier = await sdk.identifiers().create(TEST_USER_ID, { 146 | identifierType: IdentifierType.Email, 147 | identifierValue: Utils.createRandomTestEmail(), 148 | status: IdentifierStatus.Verified, 149 | }); 150 | 151 | const identifiers = await sdk.identifiers().listByValueAndType(identifier.value, identifier.type); 152 | 153 | expect(identifiers.identifiers.findIndex((rsp) => rsp.identifierID === identifier.identifierID)).toBeGreaterThan( 154 | -1, 155 | ); 156 | 157 | await sdk.identifiers().delete(TEST_USER_ID, identifier.identifierID); 158 | 159 | const newIdentifiers = await sdk.identifiers().listByValueAndType(identifier.value, identifier.type); 160 | 161 | expect(newIdentifiers.identifiers.findIndex((rsp) => rsp.identifierID === identifier.identifierID)).toEqual(-1); 162 | }); 163 | 164 | test('should handle multiple identifier deletions gracefully', async () => { 165 | expect.assertions(3); 166 | const identifiersCreatePromises: Promise[] = []; 167 | 168 | [...Array(3).keys()].forEach(() => { 169 | const identifier = sdk.identifiers().create(TEST_USER_ID, { 170 | identifierType: IdentifierType.Email, 171 | identifierValue: Utils.createRandomTestEmail(), 172 | status: IdentifierStatus.Verified, 173 | }); 174 | identifiersCreatePromises.push(identifier); 175 | }); 176 | 177 | const identifiersToDelete = await Promise.all(identifiersCreatePromises); 178 | 179 | const identifiersDeletePromises = identifiersToDelete.map((identifier) => 180 | sdk 181 | .identifiers() 182 | .delete(TEST_USER_ID, identifier.identifierID) 183 | .then(() => sdk.identifiers().listByValueAndType(identifier.value, identifier.type)) 184 | .then((ret) => 185 | expect(ret.identifiers.findIndex((rsp) => rsp.identifierID === identifier.identifierID)).toEqual(-1), 186 | ), 187 | ); 188 | 189 | await Promise.all(identifiersDeletePromises); 190 | }); 191 | 192 | test('should fail to create identifier with invalid type', async () => { 193 | expect.assertions(2); 194 | 195 | const userId = (await Utils.createUser()).userID; 196 | const req: IdentifierCreateReq = { 197 | identifierType: 'invalid_type' as IdentifierType, // Invalid identifier type 198 | identifierValue: Utils.createRandomTestEmail(), 199 | status: IdentifierStatus.Primary, 200 | }; 201 | 202 | try { 203 | await sdk.identifiers().create(userId, req); 204 | } catch (error) { 205 | expect(error).toBeInstanceOf(ServerError); 206 | expect((error as ServerError).httpStatusCode).toEqual(400); 207 | } 208 | }); 209 | 210 | test('should fail to update non-existent identifier', async () => { 211 | expect.assertions(2); 212 | 213 | const nonExistentId = 'non-existent-id'; 214 | try { 215 | await sdk.identifiers().updateStatus(TEST_USER_ID, nonExistentId, IdentifierStatus.Primary); 216 | } catch (error) { 217 | expect(error).toBeInstanceOf(ServerError); 218 | expect((error as ServerError).httpStatusCode).toEqual(400); 219 | } 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /tests/integration/services/user.test.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from '../../../src'; 2 | import { ServerError } from '../../../src/errors'; 3 | import { UserCreateReq, UserStatus } from '../../../src/generated'; 4 | import Utils from '../../utils'; 5 | 6 | describe('User Validation Tests', () => { 7 | let sdk: SDK; 8 | 9 | beforeEach(() => { 10 | sdk = Utils.SDK(); 11 | }); 12 | 13 | test('should handle null full name', async () => { 14 | expect.assertions(2); 15 | 16 | try { 17 | const req = { name: Utils.testConstants.TEST_EMPTY_STRING, status: UserStatus.Active }; 18 | 19 | await sdk.users().create(req); 20 | } catch (error) { 21 | expect(error).toBeInstanceOf(ServerError); 22 | expect((error as ServerError).httpStatusCode).toEqual(500); 23 | } 24 | }); 25 | 26 | test('should handle successful create', async () => { 27 | expect.assertions(1); 28 | 29 | const req: UserCreateReq = { fullName: Utils.createRandomTestName(), status: UserStatus.Active }; 30 | 31 | const sendResponse = await sdk.users().create(req); 32 | expect(sendResponse.fullName).toEqual(req.fullName); 33 | }); 34 | 35 | test('should handle not found delete', async () => { 36 | expect.assertions(3); 37 | try { 38 | await sdk.users().delete(Utils.testConstants.TEST_USER_ID); 39 | } catch (error) { 40 | expect(error).toBeInstanceOf(ServerError); 41 | expect((error as ServerError).httpStatusCode).toEqual(400); 42 | expect((error as ServerError).getValidationMessages()).toEqual(['userID: does not exist']); 43 | } 44 | }); 45 | 46 | test('should handle successful delete', async () => { 47 | expect.assertions(2); 48 | 49 | const userId = (await Utils.createUser()).userID; 50 | 51 | await sdk.users().delete(userId); 52 | 53 | try { 54 | await sdk.users().get(userId); 55 | } catch (error) { 56 | expect(error).toBeInstanceOf(ServerError); 57 | expect((error as ServerError).httpStatusCode).toEqual(400); 58 | } 59 | }); 60 | 61 | test('should handle not found get', async () => { 62 | expect.assertions(2); 63 | 64 | try { 65 | await sdk.users().get(Utils.testConstants.TEST_USER_ID); 66 | } catch (error) { 67 | expect(error).toBeInstanceOf(ServerError); 68 | expect((error as ServerError).httpStatusCode).toEqual(400); 69 | } 70 | }); 71 | 72 | test('should handle successful get', async () => { 73 | expect.assertions(1); 74 | 75 | const userId = (await Utils.createUser()).userID; 76 | const getResponse = await sdk.users().get(userId); 77 | expect(getResponse.userID).toEqual(userId); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/sdk.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from '../src/errors/index.js'; 2 | import { Config, SDK } from '../src/index.js'; 3 | 4 | describe('SDK class', () => { 5 | let projectID; 6 | let apiSecret; 7 | let config: Config; 8 | let sdk: SDK; 9 | 10 | beforeEach(() => { 11 | projectID = process.env.CORBADO_PROJECT_ID; 12 | apiSecret = process.env.CORBADO_API_SECRET; 13 | 14 | if (!projectID || !apiSecret) { 15 | throw new BaseError('Env Error', 5001, 'Both projectID and apiSecret must be defined', true); 16 | } 17 | 18 | config = new Config( 19 | projectID, 20 | apiSecret, 21 | `https://${projectID}.frontendapi.cloud.corbado.io`, 22 | `https://backendapi.cloud.corbado.io`, 23 | ); 24 | sdk = new SDK(config); 25 | }); 26 | 27 | it('should instantiate SDK with Configuration object', () => { 28 | expect(sdk).toBeDefined(); 29 | }); 30 | 31 | it('should create AxiosInstance with provided Configuration object', () => { 32 | const axiosInstance = sdk.createClient(config); 33 | expect(axiosInstance).toBeDefined(); 34 | }); 35 | 36 | it('should create Sessions object with created AxiosInstance', () => { 37 | const sessions = sdk.sessions(); 38 | expect(sessions).toBeDefined(); 39 | }); 40 | 41 | it('should create Users object with created AxiosInstance', () => { 42 | const users = sdk.users(); 43 | expect(users).toBeDefined(); 44 | }); 45 | 46 | it('should create Identifiers object with created AxiosInstance', () => { 47 | const identifiers = sdk.identifiers(); 48 | expect(identifiers).toBeDefined(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/setupJest.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | require('dotenv').config(); 3 | 4 | module.export = {}; 5 | -------------------------------------------------------------------------------- /tests/unit/session.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import express from 'express'; 3 | import { exportJWK, generateKeyPair, KeyLike, SignJWT } from 'jose'; 4 | // @ts-ignore 5 | import http from 'http'; 6 | import { BaseError, httpStatusCodes, ValidationError } from '../../src/errors'; 7 | import { ValidationErrorNames } from '../../src/errors/validationError'; 8 | import SessionService from '../../src/services/sessionService'; 9 | 10 | const app = express(); 11 | const PORT = 8081; 12 | const TEST_USER_ID = '12345'; 13 | const TEST_USER_FULL_NAME = 'Test Name'; 14 | 15 | let validPrivateKey: KeyLike; 16 | let invalidPrivateKey: KeyLike; 17 | let publicKeyJwk: any; 18 | 19 | async function initializeKeys() { 20 | const { privateKey: key, publicKey } = await generateKeyPair('RS256'); 21 | validPrivateKey = key; 22 | 23 | const { privateKey: invalidKey } = await generateKeyPair('RS256'); 24 | invalidPrivateKey = invalidKey; 25 | publicKeyJwk = await exportJWK(publicKey); 26 | publicKeyJwk.kid = 'kid123'; 27 | } 28 | 29 | // @ts-ignore 30 | app.get('/jwks', (req, res) => { 31 | res.json({ keys: [publicKeyJwk] }); 32 | }); 33 | 34 | async function startJWKSserver() { 35 | await initializeKeys(); 36 | return new Promise((resolve) => { 37 | const server = app.listen(PORT, () => { 38 | console.log(`JWKS server is running at http://localhost:${PORT}`); 39 | resolve(server); 40 | }); 41 | }); 42 | } 43 | 44 | async function generateJWT( 45 | issuer: string, 46 | expiresIn: number, 47 | notBefore: number, 48 | privateKey: KeyLike, 49 | alg: string, 50 | ): Promise { 51 | const payload = { 52 | sub: TEST_USER_ID, 53 | name: TEST_USER_FULL_NAME, 54 | iss: issuer, 55 | iat: Math.floor(Date.now() / 1000), 56 | exp: Math.floor(Date.now() / 1000) + expiresIn, 57 | nbf: Math.floor(Date.now() / 1000) + notBefore, 58 | }; 59 | 60 | if (alg === 'none') { 61 | const header = { 62 | alg: 'none', 63 | kid: 'kid123', 64 | }; 65 | 66 | const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url'); 67 | const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url'); 68 | 69 | return `${encodedHeader}.${encodedPayload}.`; 70 | } 71 | 72 | return await new SignJWT(payload) 73 | .setProtectedHeader({ 74 | alg: alg, 75 | kid: 'kid123', 76 | }) 77 | .sign(privateKey); 78 | } 79 | 80 | function createSessionService(issuer: string): SessionService { 81 | return new SessionService( 82 | 'cbo_session_token', 83 | issuer, 84 | `http://localhost:${PORT}/jwks`, 85 | 10, 86 | 'pro-1', 87 | ); 88 | } 89 | 90 | describe('Session Service Unit Tests', () => { 91 | let server: http.Server; 92 | 93 | beforeAll(async () => { 94 | server = await startJWKSserver(); 95 | }); 96 | 97 | afterAll(async () => { 98 | server.close(); 99 | }); 100 | 101 | test('should throw error if required parameters are missing in constructor', () => { 102 | expect(() => new SessionService('', 'https://pro-1.frontendapi.cloud.corbado.io', `http://localhost:${PORT}/jwks`, 10, 'pro-1')).toThrow( 103 | 'Required parameter is empty', 104 | ); 105 | expect(() => new SessionService('cbo_session_token', '', `http://localhost:${PORT}/jwks`, 10, 'pro-1')).toThrow( 106 | 'Required parameter is empty', 107 | ); 108 | expect(() => new SessionService('cbo_session_token', 'https://pro-1.frontendapi.cloud.corbado.io', '', 10, 'pro-1')).toThrow( 109 | 'Required parameter is empty', 110 | ); 111 | }); 112 | 113 | test('should throw ValidationError if JWT is empty', async () => { 114 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 115 | 116 | await expect(sessionService.validateToken('')).rejects.toThrow(BaseError); 117 | await expect(sessionService.validateToken('')).rejects.toHaveProperty( 118 | 'statusCode', 119 | httpStatusCodes.EMPTY_STRING.code, 120 | ); 121 | }); 122 | 123 | test('should throw ValidationError if JWT is too short', async () => { 124 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 125 | 126 | await expect(sessionService.validateToken('short')).rejects.toThrow(ValidationError); 127 | await expect(sessionService.validateToken('short')).rejects.toHaveProperty( 128 | 'name', 129 | ValidationErrorNames.InvalidShortSession, 130 | ); 131 | }); 132 | 133 | test('should throw ValidationError if JWT has an invalid signature', async () => { 134 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 135 | 136 | const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtpZDEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGguYWNtZS5jb20iLCJpYXQiOjE3MjY0OTE4MDcsImV4cCI6MTcyNjQ5MTkwNywibmJmIjoxNzI2NDkxNzA3LCJzdWIiOiJ1c3ItMTIzNDU2Nzg5MCIsIm5hbWUiOiJuYW1lIiwiZW1haWwiOiJlbWFpbCIsInBob25lX251bWJlciI6InBob25lTnVtYmVyIiwib3JpZyI6Im9yaWcifQ.invalid'; 137 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 138 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty( 139 | 'name', 140 | ValidationErrorNames.JWTInvalid, 141 | ); 142 | }); 143 | 144 | test('should throw ValidationError using invalid private key', async () => { 145 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 146 | const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 0, invalidPrivateKey, 'RS256'); 147 | 148 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 149 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.JWTInvalid); 150 | }); 151 | 152 | test('should throw ValidationError using alg "none"', async () => { 153 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 154 | const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 0, invalidPrivateKey, 'none'); 155 | 156 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 157 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.JWTInvalid); 158 | }); 159 | 160 | test('should throw ValidationError if JWT is not yet valid (nbf claim in the future)', async () => { 161 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 162 | const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 600, validPrivateKey, 'RS256'); 163 | 164 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 165 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty( 166 | 'name', 167 | ValidationErrorNames.JWTClaimValidationFailed, 168 | ); 169 | }); 170 | 171 | test('should throw ValidationError using an expired JWT', async () => { 172 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 173 | const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', -600, 0, validPrivateKey, 'RS256'); 174 | 175 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 176 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.JWTExpired); 177 | }); 178 | 179 | test('should throw ValidationError if issuer is empty', async () => { 180 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 181 | const jwt = await generateJWT('', 600, 0, validPrivateKey, 'RS256'); 182 | 183 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 184 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.EmptyIssuer); 185 | }); 186 | 187 | test('should throw ValidationError if issuer is mismatch 1', async () => { 188 | const sessionService = createSessionService('https://pro-1.frontendapi.corbado.io') 189 | const jwt = await generateJWT('https://pro-2.frontendapi.cloud.corbado.io', 600, 0, validPrivateKey, 'RS256'); 190 | 191 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 192 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.InvalidIssuer); 193 | }); 194 | 195 | test('should throw ValidationError if issuer is mismatch 2', async () => { 196 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 197 | const jwt = await generateJWT('https://pro-2.frontendapi.corbado.io', 600, 0, validPrivateKey, 'RS256'); 198 | 199 | await expect(sessionService.validateToken(jwt)).rejects.toThrow(ValidationError); 200 | await expect(sessionService.validateToken(jwt)).rejects.toHaveProperty('name', ValidationErrorNames.InvalidIssuer); 201 | }); 202 | 203 | test('should return user using old Frontend API URL as issuer in JWT', async () => { 204 | const sessionService = createSessionService('https://pro-1.frontendapi.cloud.corbado.io') 205 | const jwt = await generateJWT('https://pro-1.frontendapi.corbado.io', 600, 0, validPrivateKey, 'RS256'); 206 | 207 | const user = await sessionService.validateToken(jwt); 208 | expect(user.userId).toBe(TEST_USER_ID); 209 | expect(user.fullName).toBe(TEST_USER_FULL_NAME); 210 | }); 211 | 212 | test('should return user using old Frontend API URL as issuer in config', async () => { 213 | const sessionService = createSessionService('https://pro-1.frontendapi.corbado.io') 214 | const jwt = await generateJWT('https://pro-1.frontendapi.cloud.corbado.io', 600, 0, validPrivateKey, 'RS256'); 215 | 216 | const user = await sessionService.validateToken(jwt); 217 | expect(user.userId).toBe(TEST_USER_ID); 218 | expect(user.fullName).toBe(TEST_USER_FULL_NAME); 219 | }); 220 | 221 | test('should return user data using CNAME', async () => { 222 | const sessionService = createSessionService('https://auth.acme.com') 223 | const jwt = await generateJWT('https://auth.acme.com', 600, 0, validPrivateKey, 'RS256'); 224 | 225 | const user = await sessionService.validateToken(jwt); 226 | expect(user.userId).toBe(TEST_USER_ID); 227 | expect(user.fullName).toBe(TEST_USER_FULL_NAME); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import AxiosMockAdapter from 'axios-mock-adapter'; 2 | import axios, { AxiosInstance } from 'axios'; 3 | import { SDK, Config } from '../src'; 4 | import { BaseError, httpStatusCodes } from '../src/errors'; 5 | import { User } from '../src/generated'; 6 | 7 | class Utils { 8 | public static SDK(): SDK { 9 | const config = new Config( 10 | this.getEnv('CORBADO_PROJECT_ID'), 11 | this.getEnv('CORBADO_API_SECRET'), 12 | this.getEnv('CORBADO_FRONTEND_API'), 13 | this.getEnv('CORBADO_BACKEND_API'), 14 | ); 15 | 16 | return new SDK(config); 17 | } 18 | 19 | public static AxiosInstance(): AxiosInstance { 20 | const instance = axios.create({ 21 | baseURL: process.env.CORBADO_BACKEND_API, 22 | auth: { 23 | username: process.env.CORBADO_PROJECT_ID!, 24 | password: process.env.CORBADO_API_SECRET!, 25 | }, 26 | }); 27 | 28 | return instance; 29 | } 30 | 31 | public static MockAxiosInstance = () => { 32 | const axiosInstance = axios.create(); 33 | const mock = new AxiosMockAdapter(axiosInstance); 34 | return { axiosInstance, mock }; 35 | }; 36 | 37 | public static restoreMock = (mock: AxiosMockAdapter) => { 38 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 39 | mock.restore(); 40 | }; 41 | 42 | private static getEnv(key: string): string { 43 | const value = process.env[key]; 44 | if (!value) { 45 | throw new BaseError( 46 | `Environment variable ${key} not found`, 47 | httpStatusCodes.NOT_FOUND.code, 48 | httpStatusCodes.NOT_FOUND.description, 49 | httpStatusCodes.NOT_FOUND.isOperational, 50 | ); 51 | } 52 | 53 | return value; 54 | } 55 | 56 | public static generateString(length: number): string { 57 | // Removed I, 1, 0, and O because of risk of confusion 58 | const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; 59 | const charactersLength = characters.length; 60 | 61 | let result = ''; 62 | for (let i = 0; i < length; i += 1) { 63 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 64 | } 65 | 66 | return result; 67 | } 68 | 69 | private static generateNumber(length: number): string { 70 | const characters = '0123456789'; 71 | const charactersLength = characters.length; 72 | 73 | let result = ''; 74 | for (let i = 0; i < length; i += 1) { 75 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 76 | } 77 | 78 | return result; 79 | } 80 | 81 | public static createRandomTestName(): string { 82 | return this.generateString(10); 83 | } 84 | 85 | public static createRandomTestEmail(): string { 86 | return `integration-test+${this.generateString(10)}@corbado.com`; 87 | } 88 | 89 | public static createRandomTestEmailID(): string { 90 | return `eml-${this.generateNumber(11)}`; 91 | } 92 | 93 | public static createRandomTestPhoneNumber(): string { 94 | return `+491509${this.generateNumber(7)}`; 95 | } 96 | 97 | public static async createUser(): Promise { 98 | const rsp = await this.SDK().users().createActiveByName(this.createRandomTestName()); 99 | 100 | return rsp; 101 | } 102 | 103 | public static testConstants = { 104 | TEST_RUNTIME: '1234', 105 | TEST_REMOTE_ADDRESS: '124.0.0.1', 106 | TEST_USER_AGENT: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', 107 | TEST_REDIRECT_URL: 'https://example.com', 108 | TEST_EMPTY_STRING: '', 109 | TEST_EMAILLINK_ID: 'eml-123456789', 110 | TEST_EMAILOTP_ID: 'emc-123456789', 111 | TEST_SMSOTP_ID: 'sms-123456789', 112 | TEST_USER_ID: 'usr-123456789', 113 | TEST_PHONENUMBER_ID: '123456789', 114 | TEST_TOKEN: 'fdfdsfdss1fdfdsfdss1', 115 | }; 116 | } 117 | 118 | export default Utils; 119 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "$schema": "https://json.schemastore.org/tsconfig", 3 | // "display": "Corbado NodeJS", 4 | // "compilerOptions": { 5 | // "lib": ["DOM", "ES2020"], 6 | // "module": "esnext", 7 | // "target": "es2020", 8 | 9 | // "strict": true, 10 | // "noFallthroughCasesInSwitch": true, 11 | // "experimentalDecorators": true, 12 | // "moduleResolution": "node", 13 | // "sourceMap": true, 14 | // "outDir": "esm", 15 | // "baseUrl": ".", 16 | // "paths": { 17 | // "src/*": ["src/*"] 18 | // }, 19 | 20 | // "declaration": true 21 | // }, 22 | // "include": ["src/**/*"] 23 | // } 24 | 25 | { 26 | "$schema": "https://json.schemastore.org/tsconfig", 27 | "display": "Corbado NodeJS - Base", 28 | "compilerOptions": { 29 | "lib": ["DOM", "ES2020"], // Standard library files to be included in the compilation. 30 | "target": "es2020", // Specify ECMAScript target version 31 | "strict": true, // Enable all strict type-checking options 32 | "noFallthroughCasesInSwitch": true, // Prevent fall-through case in switch statements 33 | "experimentalDecorators": true, // Enable experimental support for decorators 34 | "moduleResolution": "node", // Resolve modules using Node.js style 35 | "sourceMap": true, // Generates source map files for debugging 36 | "baseUrl": ".", // Base directory to resolve non-relative module names 37 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 38 | "paths": { 39 | "src/*": ["src/*"] // Specify paths for modules 40 | }, 41 | "declaration": true // Generates corresponding '.d.ts' file for each output '.js' file 42 | }, 43 | "ts-node": { 44 | "moduleTypes": { 45 | "jest.config.ts": "cjs" 46 | } 47 | }, 48 | "include": ["src/**/*"] // Specify files to be included in the compilation 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", // Generate CommonJS style modules 5 | "outDir": "cjs" // Output directory for CommonJS modules 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*.ts", "lib/**/*.js", "tests/**/*.ts", "tests/setupJest.*", "jest.config.js"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "outDir": "esm", 6 | "esModuleInterop": true 7 | } 8 | } 9 | --------------------------------------------------------------------------------