├── .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 |
2 |
3 | # Corbado Node.js SDK
4 |
5 | [](./LICENSE)
6 | 
7 | [](https://nolleh.gitcorbado/corbado-nodejs/badges/coverage-jest%20coverage.svg?raw=true)
8 | [](https://codecov.io/gh/corbado/corbado-nodejs)
9 | [](https://apireference.cloud.corbado.io/backendapi-v2/)
10 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------